Skip to content

Sudoers: drop passwordless disablesleep 1; Start asks for the administrator password - #32

Open
krishhgg wants to merge 55 commits into
mainfrom
fix/sudoers-no-passwordless-sleep-off
Open

krishhgg wants to merge 55 commits into
mainfrom
fix/sudoers-no-passwordless-sleep-off

Conversation

@krishhgg

@krishhgg krishhgg commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Why

After a normal install, any program running as the user could run sudo -n pmset -a disablesleep 1 with no password and with Insomnia not running. Sleep turned off that way is never journaled, so the recovery agent does not undo it, and the grant lasted until uninstall. A laptop could stay awake in a bag until someone noticed. The review marked this a launch blocker.

A root helper that only accepts the signed app needs a stable Developer ID signature the project does not have yet. This PR takes the maintainer's chosen route instead: drop the one dangerous sudoers line and ask for the administrator password each time a session turns sleep off.

What

  • scripts/install.sh writes three sudoers lines (disablesleep 0, lowpowermode 1, lowpowermode 0) instead of four. None of them can keep the Mac awake. Turning sleep back on and the Low Power Mode floor stay passwordless so the app, backstop.sh and uninstall.sh can recover unattended. The rule is printed by one function and always rewritten, so a reinstall over an older four-line file drops the old line. Its check of the rule now runs sudo -k -n /usr/bin/pmset -a disablesleep 0, and only while pmset -g reports SleepDisabled 0 or no SleepDisabled line (fifth round). Review fixes: the installer never writes disablesleep 1, on any path. It asks for the password first (sudo -v), so a cancelled or failed password changes nothing and a running session keeps going. Before that, when session.json holds a future deadline, it prints "A session is running and the upgrade will end it." and, with a terminal on stdin, asks "Continue? [y/N]"; anything but y stops before any sudo call. Then it asks a running app to quit and stops with nothing changed, the sudoers file included, if the app is still running after 15 s. Then it writes the rule on sudo's cached credential (sudo -n -v checks it; one more prompt only if it expired during the quit), checks that the app was not opened again meanwhile, and replaces the bundle. Any stop between the rule and the new bundle prints a note that an older build cannot start a session until the rerun, with the rerun command; an EXIT trap armed for exactly that window prints it. A successful install still writes the file once.

  • New Sources/Insomnia/System/AdministratorPrompt.swift: AdministratorPromptRunning protocol, a fixed script literal, a 120 s limit, and OsascriptAdministratorPrompt, which runs /usr/bin/osascript -e <literal> <marker> <nonce> as a child and sends SIGTERM only at the deadline. The literal is do shell script "/usr/bin/lockf -k -n -t 10 " & quoted form of (item 1 of argv) & " /bin/sh -c " & quoted form of "<rootCommand>" & " insomnia " & quoted form of (item 1 of argv) & " " & quoted form of (item 2 of argv) with administrator privileges with prompt "Insomnia needs your password to turn off system sleep for this session.". AdministratorPrompt.rootCommand is fixed text: it runs /usr/bin/pmset -a disablesleep 1 only while the pending-start marker ($1) holds the nonce ($2), and exits 3 otherwise. Second review round: lockf (AdministratorPrompt.markerLock) holds the marker's flock from before that check until pmset exits, never creates the file (exit 69 when it is missing) and gives up after 10 s (exit 75). The second read after pmset and its compensating disablesleep 0 are gone. Review fix: the wait is bounded. Both pipes are drained on their own threads; 3 s after the deadline (AdministratorPrompt.stopGrace, the backstop's grace) a caller still waiting gets AdministratorPromptError.stillRunning with an UnfinishedPrompt handle carrying osascript's pid, osascriptAlive, waitUntilOsascriptExits(), which resolves as soon as osascript itself is reaped, and waitUntilExit(), which resolves once its output has closed too. Nothing is ever killed.

  • Pending-start marker (Codex P0). performStart writes a fresh UUID to APP_SUPPORT/pending-start (atomic, no newline) right before the dialog and deletes it on every outcome before it releases the recovery lock; on stillRunning it deletes it first (third round: a start whose deletion succeeded no longer waits; see below). exclusive() deletes a leftover marker right after it takes the lock, so reconcile and every other app transaction void a dialog left from a start that died. backstop.sh deletes it right after lockf, before any decision or sudo call, and logs it; install.sh reaches that through the backstop it runs under its lock. uninstall.sh deletes it right after its own lock, before it runs a backstop that may be an older copy, and journal_problems refuses to remove anything while it is still there. A marker that cannot be written rolls the start back without showing the dialog. New Paths.pendingStartFile, Store.savePendingStart and Store.removePendingStart. Second review round: every deleter takes the marker's own lock before it unlinks, so the file never goes between the root command's check and the end of pmset. Store.removePendingStart replaces deletePendingStart: flock, polled for up to 10 s, then .markerBusy; a missing file or a link to nothing is not an error. Both scripts run "$LOCKF" -k -n -s -t 10 "$PENDING" "$RM" -f "$PENDING", through a new RM=/bin/rm (Greptile). A marker that cannot be locked or deleted leaves recovery incomplete. The app still restores sleep, but keeps sleepDisabledByUs, reports it (log, "Restore incomplete", a menu line naming the file), refuses new starts with "Session not started", and retries on every transaction. backstop.sh restores sleep, keeps the entry, lists the marker among its failures and exits 1, also when the journal is otherwise clean.

  • PmsetSleepGuard takes the runner by injection. SleepGuarding.setSleepDisabled(Bool) is split into disableSleep(PendingStart), which goes through the dialog, and enableSleep(), which runs sudo -n pmset -a disablesleep 0, so no path can turn sleep off without a marker. The Low Power Mode calls still use sudo -n.

  • SessionManager.performStart is unchanged in order (session.json, journal, backstop armed, then the dialog). A wrong password, a timeout and a pmset failure take the existing startFailed path, which undoes from the journal (disablesleep 0); the notification names the password prompt. Second review round (Codex P1): a cancel (osascript's stderr ends in (-128)) and an osascript that never launched ran nothing as root, so the start puts session.json and the journal back exactly as it read them and runs no pmset; a SleepDisabled another tool set stays, and the notification says nothing was changed. On stillRunning it deletes the marker (or, while the dialog's root command holds its lock, deletes it once the prompt exits), posts "Password prompt still running" with the pid, and sets the menu warning line (the only place that offers kill <pid>; a notification outlives the pid). Codex P1 fix: it then waits for osascript's own exit and replaces the line with "osascript (pid N) stopped, but a command it started as root is still running" if the output is still held, so the line never offers a kill for a pid that is gone. It keeps session.json, the journal entry and the recovery lock until the output closes, then runs the same startFailed rollback. Starts, ends and the agent queue behind it.

  • SessionManager.performReconcile step 2 no longer calls setSleepDisabled(true). It reads pmset -g. SleepDisabled 1 continues the session (journal, backstop, timers as before, no prompt). SleepDisabled 0 ends it with the new EndReason.sleepReenabled and the notification "Sleep was turned back on while Insomnia was not running, so the session ended." A failed read ends it with recoveryUnavailable. Start is the only caller of disableSleep(_:).

  • Docs: README (install paragraph, "Exactly what gets installed", Start step, recovery section), SECURITY.md (what the remaining lines allow, the pending-start check), spec sections 1, 2, 8 (a new invariant for the marker), 9, install and manual plan, and thirteen "Not run" rows in docs/release-validation.md. Second review round: the README Start step says a cancel changes nothing while a wrong password or a timeout runs disablesleep 0, and describes an undeletable marker; SECURITY.md and spec section 8 describe the lock, and section 8 gains an invariant for clearing sleepDisabledByUs; the cancel row is split from the wrong-password row, and a new row covers an immutable marker.

  • .greptile/config.json and .greptile/rules.md (from CI: bash 3.2 syntax check, warnings as errors, workflow lint; Greptile project config #31) described the four-line grant this PR removes. The rule is now sudoers-rule-is-three-exact-lines: the three commands, and disablesleep 1 never passwordless on any install.sh path, only through AdministratorPrompt.disableSleepScript. The instructions and the journal-before-change, backstop-armed and never-SIGKILL rules name the prompt path; rules.md describes it and lists the three-line rule, the bounded prompt and the new install order as deliberate. The rest of each rule is unchanged. For the Codex fixes, no-shell-interpolation now names rootCommand and says the marker path and nonce reach it only as $1 and $2 through quoted form of, a new abandoned-password-prompt-cannot-turn-sleep-off rule states the marker invariant, and rules.md lists the marker and the menu line change as deliberate. Second review round: the abandoned-prompt rule describes the lock, the lock-then-unlink deleters, the keep-the-entry gate, the start refusal and the cancel rollback without pmset, and no-shell-interpolation names markerLock.

  • Third review round (Codex and Greptile at b180529), fixed in aced667 unless a bullet names another commit:

    • Start shows no dialog unless the installed backstop.sh deletes pending-start. backstop.sh now carries # insomnia-backstop-version: 2. New BackstopVersion.swift reads it through LaunchdBackstop.checkVoidsPrompts(), the first step of performStart after the marker-problem refusal, before anything is written. A missing line, a lower number or an unreadable script refuses Start: "start refused, nothing changed: the installed backstop.sh at is older than this build and cannot cancel a password dialog left open by a crash; run scripts/install.sh again", plus a "Session not started" notification.
    • install.sh installs the new backstop.sh before it publishes the bundle. After the "opened again" check it takes the recovery lock and runs "$INSTALL" -S -m 0755 on the script. That is a rename, so a run of the old copy keeps its own inode. It then waits up to RETIRE_WAIT_SECONDS (30) until pgrep -lf shows no process running $APP_SUPPORT/backstop.sh. If one stays, or pgrep fails, it stops before the bundle and says that the rule and the new backstop.sh are installed and the app and the LaunchAgent were not. Then come the bundle, recovery (backstop --force) and the LaunchAgent, as before.
    • A stuck prompt is rolled back at once when clearPendingStart() removed its marker under the marker's lock. Its root command can no longer change anything, so the transaction restores sleep, finishes and releases the recovery lock. watchVoidedPrompt follows the leftover process outside the transaction: the menu line drops the kill <pid> hint once osascript exits and goes once the whole prompt has, unless something else has replaced it. A prompt whose command holds the marker's lock (past its checks, maybe in pmset), or whose marker cannot be deleted, is still waited for as before.
    • The root command gets the session's endsAt as $3: PendingStart.deadline in whole seconds, rounded down, passed as item 3 of argv through quoted form of. It refuses with exit 4, "the session this password was for has already ended; sleep was not turned off", unless /bin/date +%s is below it. A $3 that [ cannot compare fails the test, so it refuses too. The command is still one fixed literal.
    • restoreAll no longer drops a failed journal write after a successful enableSleep() with try? (Greptile). It calls fail(...) with "sleep restored but the journal entry could not be cleared: ; it will be retried". That logs the error, sets the menu line, and makes the end report itself incomplete. The entry stays and the next run retries.
    • install.sh calls rm, rmdir, mkdir, cp and install through fixed variables (RM, RMDIR, MKDIR, CP, INSTALL), its two EXIT traps included (Greptile). In 241ea92 the LaunchAgent move, mktemp and the cat that writes the temporary sudoers file follow (MV, MKTEMP, CAT), and sudo is handed "$VISUDO" and "$INSTALL", because with a bare name sudo searches the caller's PATH and runs what it finds as root. In afc4948 uninstall.sh hands sudo "$TEST" (TEST=/bin/test) and "$RM" the same way.
    • The osascript runner waits for osascript through ProcessExit (from Process: wait for children with an exit handler, not waitUntilExit #46) instead of process.waitUntilExit() on a GCD worker, which Process: wait for children with an exit handler, not waitUntilExit #46 found can wait forever on macOS 26 after the child has exited. A Start whose prompt never reported its exit would have kept the recovery lock (887bc71).
    • Docs: the README Start step, install paragraph and stuck-commands limit; SECURITY.md (the deadline, the version check and the install order); spec sections 1, 2, 8 (the deadline and lock invariants, the journal-clear report) and 9; three new "Not run" rows and one changed. .greptile: the lock rule names the voided prompt as the one command that no longer holds the lock; the fixed-path rule lists the new variables and covers EXIT traps and commands run through sudo; rules.md describes the voided rollback, the deadline and the backstop version contract.
  • Fourth review round (local Codex and Greptile, both at 887bc71):

    • Start refuses unless sleep can be turned back on without a password (Codex P1, 1b36a53). New SleepGuarding.checkPasswordlessRestore(). PmsetSleepGuard runs sudo -n -l /usr/bin/pmset -a disablesleep 0, which lists the rule without running pmset and never prompts. performStart calls it right after the backstop version check, before anything is written or shown. A non-zero exit, or a sudo that cannot run, refuses Start with "start refused, nothing changed: sleep can only be turned off while it can be turned back on without a password, and sudo -n -l ... did not confirm that (); /etc/sudoers.d/insomnia is missing or not in effect, run scripts/install.sh again", plus a "Session not started" notification. PmsetSleepGuard takes the sudo path by injection, so tests use a fake sudo, and its restore uses the same argument list as the check. The fifth round replaces the listing with a run of the restore (below).
    • Every marker deleter checks that the path still names the file it locked (Greptile P0, 2ed716d). Store.removePendingStart compares fstat of its locked descriptor with stat of the path, which follows links as lockf does. It unlinks only when they match, otherwise looks again, and throws .markerReplaced at the limit. savePendingStart returns the device and inode it wrote. A start that voids its own stuck prompt passes them as expecting:, so a replaced or missing marker does not count as voided and the start waits for the prompt. backstop.sh and uninstall.sh share a delete_pending_marker function. It opens the marker on fd 8 (regular files only), locks it with "$LOCKF" -s -t 10 8, compares "$STAT" -f %d:%i <&8 with "$STAT" -L -f %d:%i of the path, and only then runs "$RM" -f. STAT=/usr/bin/stat is new. A mismatch counts as a stuck marker.
    • backstop.sh exits 1 on a stuck marker on every path (Greptile P2, 6e1450a). The valid-session branch and the branch that moves a malformed session.json aside both exited 0 before the check. Every exit 0 after the marker step now goes through exit_unless_marker_stuck.
    • install.sh retire wait and order (two Greptile P1s, e1e56b0). The wait counts only a run: arguments exactly /bin/bash $APP_SUPPORT/backstop.sh, with or without --force. The sudoers rule is now written after the wait, still under the recovery lock. The order is password, quit, lock, backstop.sh, wait, rule, bundle. A stop before the rule leaves the old rule beside the old app and says that only backstop.sh changed. The prompt for an expired credential moves to right after the quit. The installer also looks for a running Insomnia after the wait, where it stops before the rule, and right before it removes the bundle, where it stops with the rerun note.
    • Docs: README install paragraph and Start step. SECURITY.md: the restore check and what sudo -l cannot see (f72ba11), the path check and the deleters that cannot compare a written identity (04e188e), and the install order. Spec sections 2 and 8. rules.md and config.json: the sudoers rule names the sudo -n -l check and the install order, the marker rule names the identity checks and the backstop's exit 1, and the fixed-path list gains STAT. One new "Not run" row (the sudoers file moved aside) and one changed (an upgrade stopped before the rule).
  • Fifth review round (local Codex P0 and Greptile 4171411041, both at f72ba11):

    • Start proves the passwordless restore by running it (926d639). sudo -l lists a command the admin group may run with its password, and it lists without a password whenever any NOPASSWD entry exists, so the listing passed without Insomnia's rule. A cached credential passed too. checkPasswordlessRestore(sleepOffIsOurs:) now runs /usr/bin/sudo -k -n /usr/bin/pmset -a disablesleep 0. -k ignores a cached credential and -n fails instead of prompting, so only the sudoers policy can make it exit 0. performStart passes the journal's sleepDisabledByUs. When it is set, the next end or backstop.sh run owes the restore anyway, so the check runs it without a read. Otherwise the check reads pmset -g first. SleepDisabled 0, or no SleepDisabled line, runs the restore, which then changes nothing. SleepDisabled 1 runs nothing and refuses Start with "start refused, nothing changed: sleep is already off (pmset reports SleepDisabled 1) and Insomnia did not turn it off, so Start leaves it alone ... To re-enable sleep: sudo pmset -a disablesleep 0, then start again". A pmset -g that cannot be read also runs nothing and refuses Start. Nothing parses sudo -l output.
    • The foreign-sleep notification now ends "Insomnia cannot start a session until then." instead of saying that ending an Insomnia session sets it to 0.
    • install.sh had the same root cause, because its verify step ran sudo -n -l (926d639). It now reads pmset -g through a fixed PMSET=/usr/bin/pmset and runs "$SUDO" -k -n /usr/bin/pmset -a disablesleep 0 only while SleepDisabled reads 0 or has no line. A non-zero value or an unreadable pmset -g prints "sudoers rule not checked: ..." and the install goes on, because the app checks before every Start. A failed run still stops before the bundle.
    • Session file follow-ups to #23: one date form, and a kept malformed file holds the end #47 on main added testSessionWithOffsetDatesIsResumed, which expects a relaunch to run disablesleep 1 as main does. This branch never prompts on relaunch, so the test now seeds the journal and SleepDisabled 1 and expects one pmset -g and no dialog (a1f9f82).
    • Docs: the README Start step and restore check; SECURITY.md, where the run-based check replaces the gap f72ba11 described, with the SleepDisabled handling, the read-to-run window and the installer check; spec section 2 (session start), section 8 step 3 (Start is refused while another tool's 1 stays) and the manual test plan; rules.md and config.json (the sudoers rule names the run); release-validation.md, where the cancel row changes and three "Not run" rows are new (SleepDisabled 1 set by hand, another NOPASSWD entry plus sudo -v, install.sh with SleepDisabled 1).
  • Sixth review round (Greptile 4171743070 and 4171743074, both at 12c8e40), fixed in 23d625f after the two merges below:

    • Start runs no sudo before the dialog (4171743070). The pre-dialog sudo -k -n run went through CancellableCommand, which sends SIGKILL a second after SIGTERM. It is gone, so there is no privileged preflight left to time out. checkPasswordlessRestore is replaced by SleepGuarding.checkSleepSettingForStart(sleepOffIsOurs:), which only reads pmset -g. A SleepDisabled 1 the journal does not claim, or an unreadable setting, refuses Start with nothing run, and the read is skipped when the journal already owns the bit. The prompt stays SIGTERM-only and keeps the recovery lock while its command may still act.
    • The passwordless-restore proof moves into the root command (4171743074). After the nonce and deadline checks, under the marker's lock and at a point the start has already journaled, it runs /usr/bin/sudo -n -u "#$4" /usr/bin/sudo -k -n /usr/bin/pmset -a disablesleep 0. $4 is the uid of the user who pressed Start (getuid(), passed as item 4 of argv through quoted form of). Root drops to that user without a password, and the user's sudo runs the exact restore with -k and -n, so only the sudoers policy can pass it. Only on exit 0 does /usr/bin/pmset -a disablesleep 1 run, without exec, so pmset's own status cannot read as 5. Otherwise the command exits 5. OsascriptAdministratorPrompt maps that to the new AdministratorPromptError.restoreNeedsPassword, and nothingToUndo (renamed from nothingRan) is true for it, so the start puts session.json and the journal back exactly and runs no pmset. The message is "sleep was not turned off: turning it back on needs a password (sudo -k -n /usr/bin/pmset -a disablesleep 0 failed: ...), so a session could not end without you. /etc/sudoers.d/insomnia is missing or not in effect; run scripts/install.sh again". A uid that is not a positive whole number exits 5 without running sudo.
    • This does not close the race Greptile describes, because pmset has no compare-and-set. It moves the one run of the restore that nothing journaled to a point the journal covers. Not covered says what remains.
    • install.sh, as merged from Backstop: run only the copy sealed in the signed bundle #28, runs no pmset. It writes the rule under the lock with sudo -n and checks a sudo -k -n -l listing. Every one of those calls goes through Backstop: run only the copy sealed in the signed bundle #28's supervisor: SIGTERM only, with fd 9 kept until the call exits. Its comment and success line no longer present the listing as proof.
    • BackstopVersion reads the copy sealed in the bundle (Backstop: run only the copy sealed in the signed bundle #28's LaunchdBackstop.scriptPath). Its doc comment says so, and testLaunchdBackstopChecksTheScriptItsAgentRuns, which the Backstop: run only the copy sealed in the signed bundle #28 merge broke, now builds the bundle's Contents/Resources (edfb2d7).
    • Docs: README (the install paragraph without the pre-Backstop: run only the copy sealed in the signed bundle #28 retire wait, the Start step, the recovery paragraph, and the "Sleep disabled by something else" limit with the residual), SECURITY.md (the check in the root command and the residual), spec sections 1, 2 and 8 (main's four-command sudo -n -l recheck becomes the rule written and listed under the lock), release-validation.md (two rows changed, two new: the order in sudo's log, and a foreign disablesleep 1 during the dialog), .greptile/config.json and rules.md.
  • Greptile review 11 (at fa281c1):

    • The root command compares the clock with the deadline again after the restore check, right before disablesleep 1, and exits 4 at or past it (4211874293). The start is then undone like an end, and the only pmset that ran is the restore that undo runs too. bf4d6e2 changed rootCommand; 2f6b11f made the AppleScript copy osascript runs match, generated from rootCommand.
  • Greptile issue comment 6046657261 (P1, outside the diff), fixed in 2b8028c. An upgrade over a build whose Info.plist has no InsomniaResumeFrozenVersion, with a frozen process journaled with startedAtMicros, stopped at the recovery before the swap. The staged backstop.sh handed that process to the installed binary, found no declaration in the installed Info.plist, and kept the entry. On this branch the three-line rule is already written at that point, so the older app could not start a session until the install finished.

    • backstop.sh has a new --own-bundle flag. With it, Insomnia --resume-frozen is the Contents/MacOS/Insomnia and the version check reads the Contents/Info.plist of the bundle the script itself sits in, not those of ~/Applications/Insomnia.app. The path comes from BASH_SOURCE[0]; no environment variable can choose it. It must be absolute and end in .app/Contents/Resources/backstop.sh, or the script exits 2 before it opens the lock file or reads the journal.
    • install.sh passes the flag to the staged copy, which codesign has already checked against the requirement, and runs it under the recovery lock it holds. The new build resumes the processes before the swap, and the old app never runs. The LaunchAgent and uninstall.sh do not pass the flag and keep using the installed app.
    • Unchanged: the InsomniaResumeFrozenVersion check (now of the staged Info.plist), the 30 s limit, the check of every answer line, the microsecond and boot-session identity checks, startedAtMicros in the journal, and the fixed tool paths. A failed, unverifiable, malformed or late answer keeps those entries, and the install stops before the swap with the previous app and LaunchAgent in place.
    • When the recovery stops a source install, the message used to offer scripts/backstop.sh --force by hand. That hands the processes to the installed build, so over a build without the interface it keeps the entries again. Both kinds of install now say only to rerun the installer, which stages and checks a new copy and runs that copy's recovery.
    • Docs: README's recovery paragraph, spec section 8, .greptile/config.json and rules.md, and a new release-validation row for a real upgrade, also from a quarantined download.
  • Independent round 14 review (P1 at 2b8028c), changed in 42e42e5. The restore proof set 0 over a SleepDisabled 1 another tool set while the dialog was up, also when the start then ran past its deadline. The change narrows this and moves the proof's write onto Insomnia's own change. It does not close it: Not covered says what remains and the decision it needs. Round 18 removes the proof's temporary 1 and root's fallback (below).

    • The root command reads pmset -g itself after the nonce, deadline and uid checks and before it changes anything. A SleepDisabled 1, or a read that fails, stops it with a new exit 6 and nothing changed. That read comes after the whole time the dialog was up, which is when the review's reproduction set the foreign 1. The start passes whether the journal already claims the 1 as item 5 of argv ($5, "1" or "0", through quoted form of). Only an exact "1" skips the reads, as Start's own read is skipped.
    • The proof now undoes a change the command made. Root runs pmset -a disablesleep 1, then the user's sudo -k -n /usr/bin/pmset -a disablesleep 0 sets it back to 0. Before, the proof wrote 0 over whatever value it found, before Insomnia had changed anything. When the proof fails, root sets 0 itself, and only then writes its message and exits 5, so a dialog whose output is closed cannot stop that restore with SIGPIPE. If root's own restore fails, the command exits 1, which the app undoes like any pmset failure.
    • After a passing proof come the deadline check (exit 4), a second pmset -g read (exit 6) and the final disablesleep 1. A 1 another tool sets after the proof is seen and left alone.
    • AdministratorPromptError.refused(rootStatus:stderr:) is new. Exits 3, 4 and 6, and lockf's 69 and 75, map to it, and exit 5 still maps to .restoreNeedsPassword. nothingToUndo is true for both, so the start puts session.json and the journal back without running pmset. None of those exits leaves a change of the command's own: it changed nothing, or the proof or root's restore set its own 1 back to 0. Before, 3, 4, 69 and 75 were undone like an end, which ran disablesleep 0 over whatever was there. rootStatus reads the status from the number osascript appends to the error. Every other status is still undone like an end.
    • The texts say what happened. Where the command turned sleep off for the check, "sleep was not turned off" becomes "sleep was not left off", and the "Session not started" notification says "Insomnia undid anything it changed" instead of "nothing was changed".
    • Docs: README (the restore check, the Start step and the limits), SECURITY.md (the root command and a new "What this design does not close" paragraph), spec section 2 and a paragraph on what is not closed, release-validation.md (the cancel row, and a new Not run row: with the rule moved aside, pmset -g read about every 0.1 s shows SleepDisabled 1 at most for the moment of the check) and .greptile/rules.md (the order step by step, which changes to flag, and the disclosed limits).
  • Recovery round 15 merged main for Backstop: the supervisor owns each undo command's limit and signal #50 (70f5fac, Merged main below). testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked now expects the root command's five pmset calls; its lock and undo assertions are unchanged. testLockSharingIgnoresAStatOnPATH is new.

  • Independent round 17 review (GPT-6.1-Sol, xhigh, needs changes at 42e42e5), changed in 576c215 and 7dcf51f. Round 16's proof turned sleep off before the passwordless restore was established (R1, P0; Greptile 4213796939). The proof and root's fallback set 0 over another tool's 1, and a refusal whose dialog output was gone died by SIGPIPE, which lockf reports as 70 and the start undid like a failure (R2, P1). A read could use up the deadline and the write still followed at or past it (R3, P1). The root command no longer runs the restore:

    • Its only write is its last step, pmset -a disablesleep 1. The temporary 1 and root's fallback 0 are gone.
    • Before the write, root drops to the user who pressed Start (sudo -n -u "#$4") and has that user's sudo answer three queries, each through /usr/bin/env -i LC_ALL=C with stdin from /dev/null. None of them runs a command. sudo -V must show sudo 1.9.15 to 1.9.x, the sudoers policy plugin of the same version, and at most the sudoers I/O and audit plugins. sudo -k -n -l must list without a password and show no "Runas and Command-specific defaults" section, because Defaults bound to a command apply when the restore runs but not to a listing. sudo -k -n -ll /usr/bin/pmset -a disablesleep 0 must print exactly six lines: Sudoers entry: /private/etc/sudoers.d/insomnia (or /etc/sudoers.d/insomnia), RunAsUsers: root, Options: !authenticate, Commands:, a tab and the restore line, and Matched: with the restore line. sudo prints the last rule that matches, the one that decides when the command runs, and prints nothing for a denial. Round 20 narrows these checks and adds two before them (below).
    • Anything else exits 5 with nothing written: an older or unknown sudo, other plugins, a path-only, truncated or extra answer, another file, run-as list or option, a denial, a failed switch to the user, a listpw setting that wants a password, any sudo error.
    • The command ignores SIGPIPE, so a refusal keeps its own status when the dialog's output is gone, and it sets LC_ALL=C for every tool it runs, all by absolute path.
    • The clock is compared with the deadline after the nonce check, after the sudo queries and right before the write, and equality refuses (exit 4). pmset -g is read once, after the queries: a 1 the journal does not own, or a failed read, exits 6, and $5 = "1" still skips the read.
    • The exits and their mapping are unchanged: 5 is .restoreNeedsPassword; 3, 4, 6, 69 and 75 are .refused; anything else is undone like an end. Every refusal now comes before any write, so the rollback runs no pmset. The texts say "sleep was not turned off" again. The exit 5 text names sudo -n /usr/bin/pmset -a disablesleep 0, says to run scripts/install.sh again if the rule is missing or not in effect, and names the sudo versions, plugins and bound Defaults the check refuses.
    • .greptile/config.json keeps the requirement that the root command check the passwordless restore before it turns sleep off and run no pmset when it is missing. Only its sentence on how changed: it names the three queries, says they run nothing and are not a run of the restore (7dcf51f), and says a generic sudo -l, sudo -v, a bare exit status or a NOPASSWD grep is not proof. .greptile/rules.md describes the new order and the disclosed limits, and no longer presents round 16's order as a deliberate exception.
    • Docs: README (the restore check and the "Sleep disabled by something else" limit), SECURITY.md (the root command and what it does not close), spec section 1 and its not-closed paragraph, release-validation.md (four rows changed, and two new Not run rows: the real sudo -V, -l and -ll output in Terminal with the macOS and sudo versions, and refusals for Defaults!/usr/bin/pmset log_output and for a later rule without NOPASSWD), and install.sh's comment and success line.
  • Independent round 19 review (GPT-6.1-Sol, xhigh, needs changes at 7dcf51f), changed in c5456f8. sudo's answers passed two setups in which the restore already failed: an approval plugin with no show_version, which sudo -V does not list and a listing never consults (F1), and user or global Defaults such as log_output with !ignore_iolog_errors and an iolog_dir it cannot create (F2). And a failure before the write could still clear a 1 another tool set while the dialog was up (F3, the round 14 and 17 ownership finding, not waived). The root command now, in this order:

    • Refuses any /private/etc/sudo.conf, a link or an empty file included (5). Plugins load only from that file, its path is compiled in (Apple's sudo-114.100.11, pathnames.h:70, with no override in the Xcode project), and macOS installs none, so without it sudo has only its built-in sudoers plugins. sudo -V skips an approval plugin whose show_version is NULL (sudo.c:1902), so no answer could show one.
    • Refuses unless /etc/pam.d/sudo has exactly one uncommented session line, macOS's own session required pam_permit.so (5). sudo opens the PAM session to run a command, never for a listing.
    • Takes only sudo 1.9.17p2, its sudoers policy plugin, grammar version 50, and at most its I/O and audit plugins (5). That is macOS 26.2's sudo and the source the checks were read against. The refusal says another version needs an Insomnia release checked against it.
    • Takes only listed Defaults in the sudo -k -n -l answer (5 otherwise, naming the entry): the environment lists, the lecture and its file, the prompt and bad-password texts, password time limit and tries, the timestamp settings, tty_tickets, pwfeedback, insults, and log_allowed and log_denied on or off. Anything else refuses, log_output, logfile, preserve_groups, group_source and ignore_logfile_errors among them, and so do a backslash, a tab, a layout it cannot read and the bound-Defaults header. macOS's own Defaults are all on the list. A log sudo cannot write stops no command while ignore_logfile_errors keeps its default (audit.c:491-508), and the list accepts no entry that changes it.
    • After every check and before it reads the setting, writes a record over the nonce, <nonce> writing, in place and as the user through root's sudo -n -u "#$4" (new exit 7 if that fails, a .refused status). Refusals 6 and 4 after it put the bare nonce back. Replaced in round 22: the record goes in a receipt only root can write, after the read (below).
  • After a wrong password, a signal, a timeout or a stuck prompt, the app reads the marker under its lock before it deletes it (RemovedMarker). Only the file this start wrote, holding exactly its nonce, counts as untouched: no command for it reached pmset -g, so the start is rolled back like a refusal, with no pmset, and a 1 another tool set meanwhile stays. A missing, replaced or changed marker is undone like an end, as before. Relaunch, backstop.sh and uninstall.sh do not read the record and stay as they were. Replaced in round 22: the receipt, not the marker, shows what the command did, and relaunch, backstop.sh and uninstall.sh settle from it (below).

  • Texts: each exit 5 message names what it found (sudo.conf, PAM, the version, the Defaults entry, the listing, the rule). The app's message says to run scripts/install.sh again if the rule is missing or not in effect, and that install.sh changes none of the other causes; SECURITY.md adds that macOS keeps its sudo on the sealed system volume.

  • Docs: README (the dialog paragraph and how recovery works), SECURITY.md (the two files, 1.9.17p2, the Defaults list, the record, exits 3 to 7, and what this does not close), spec section 1, its not-closed paragraph and a section 8 invariant for the record, .greptile/rules.md, .greptile/config.json rules 36 and 78 (the requirement unchanged), and release-validation.md (three rows changed, and two new Not run rows: another tool's 1 through a wrong password and a timeout, and an /etc/sudo.conf holding only a comment).

  • Independent round 21 review (GPT-6.1-Sol, xhigh, needs changes at c5456f8), changed in bd7db43. Finding 1 (P1, from round 20): root wrote the record into the marker as the user, in a file any process running as the user can rewrite, so a process that put the bare nonce back into the same inode after root's write made the app drop the restore it owed after an ambiguous failure, and sleep stayed off with no journal entry. Finding 2 (P1): relaunch, backstop.sh and uninstall.sh never read the record, so the session of an abandoned dialog was resumed on another tool's 1 while it was valid, or undone as Insomnia's own once it had expired. Finding 3 (P2, with Greptile 4215430637): README and SECURITY.md promised more than the code does. The record moves to a receipt only root can write, and every reader settles from it:

    • The receipt is /private/var/db/com.kgarg.insomnia/<uid>: root's, mode 0644, one link, exactly 45 bytes, holding a nonce, a space, writing or refused, and a newline. install.sh creates the folder with sudo -n /bin/mkdir -m 0755 and the file with sudo -n /usr/bin/install -m 0644 -o root -g wheel, holding 00000000-0000-0000-0000-000000000000 refused, under the recovery lock after the sudoers rule. It checks every folder from /private/var/db up to / by lstat first, and the folder and file again after. A receipt already as install.sh makes it is kept. Anything else (another owner, group or other write permission, a link, the wrong size or link count, an access control entry that allows anything) stops the install, and install.sh changes neither its owner nor its mode.
    • The root command never writes the marker now, and root writes nothing in the user's folders. After the sudo checks and the deadline check that follows them, it checks that the uid is plain digits and the nonce an uppercase UUID, then checks the receipt and every folder up to / with one lstat-based stat and ls -lde (exit 7 for any failure, nothing written). Then it reads pmset -g (exit 6, nothing written). Then it writes <nonce> writing over the 45 bytes with dd conv=notrunc,fsync, which keeps the inode and returns after fsync(2), and reads the bytes back. A failed write or readback writes <nonce> refused and exits 7. The deadline check that comes next writes <nonce> refused and exits 4. The last step is still pmset -a disablesleep 1.
    • Start refuses, with nothing written and no dialog, while the receipt or a folder above it is missing or unsafe (the message says to run install.sh again), and while the journal still records an earlier start.
    • state.json gains sleepOffAttempt: the nonce, owedBefore (sleepDisabledByUs before the start), the receipt's device:inode when the start began, the deadline in whole seconds, and the marker's device:inode. Start journals it with sleepDisabledByUs before session.json, and adds the marker's identity before the dialog. A start that finishes or rolls back removes it.
    • The verdict is read only once the marker the start wrote is gone under its lock. No marker journaled means no dialog was shown, so nothing ran as root. A marker that went but is not the file the start wrote shows nothing, and so does a receipt that is another file now, fails a check, or does not hold one valid line. Another start's nonce, or this nonce with refused, shows that the command never turned sleep off. This nonce with writing shows that it may have. SleepOffReceipts.live trusts uid 0 alone. Tests build a SleepOffReceipts with their own uid for a folder in their temporary directory, and patch the root command's -v o=0 and the scripts' RECEIPTS and RECEIPT_OWNER lines in private copies; nothing in the shipped build reads a setting that widens the trust.
    • Live failures (a wrong password, a signal, a timeout, a stuck prompt) read the receipt after the marker goes. "Never" rolls the start back with no pmset; anything else is undone like an end. RemovedMarker.isUntouched is gone.
    • Settlement. Whoever next holds the recovery lock settles a start the journal still records, right after deleting its marker: an app transaction (a relaunch included), backstop.sh (version 3) or uninstall.sh. A session.json whose end equals the attempt's deadline goes, whatever the receipt shows, so that session is never resumed. "Never" puts sleepDisabledByUs back to owedBefore, which keeps a restore an earlier session still owes; anything else sets it and the restore runs. If session.json or the journal cannot be written, the attempt stays: the app ends an unexpired session instead of resuming it and refuses Start, backstop.sh undoes the journal as --force would and exits 1, and uninstall.sh stops with "Nothing was removed; rerun." The next run settles again, then with no marker of its own to match, so as "may have".
    • A marker that goes with no journaled attempt (left by an older build, or by a start that could not delete it) takes session.json with it. The app does this only when no session is in memory.
    • uninstall.sh removes the receipt with /bin/rm -f and the folder with /bin/rmdir through sudo, once the folders above it pass the same checks. It keeps the folder while another account's receipt is in it, and leaves a receipt that is not a regular file.
    • Docs: README (the Start step and its outcomes, the receipt, the foreign-sleep note, the Defaults paragraph, the install table and Uninstall), SECURITY.md (the receipt, the order, no time limit on the queries, settlement, and what this does not close), spec sections 1, 2 and 8, release-validation.md (three rows changed and five new Not run rows), .greptile/rules.md, and .greptile/config.json rules 36 and 78 (the receipt and settlement; the requirement is unchanged).
  • Independent round 23 review (GPT-6.1-Sol, xhigh, needs changes at bd7db43), changed in 7e3ddc8 and e741a7f, with main merged in 15c2fc3. Finding 1 (P1): two Insomnia folders of one user shared the receipt, so a second start's line made the first start's settlement drop a restore it owed. Finding 2 (P1): a reader that deleted a replaced marker could finish recovery while the root command, holding the original marker, could still write. Finding 3 (P1): a settled start's dialog could still be answered after its marker was recreated. Finding 4 (P1): a never-write settlement that could not be written restored sleep and cleared another tool's 1. Finding 5 (P1): dd's fsync is not a drive flush. Finding 6 (P1, the bot's P0 privilege claim not established): backstop.sh and uninstall.sh ran cat and head through PATH. Finding 8 (P2): docs promised more than the code does. Finding 7, ownership, stays open and unwaived (Not covered).

    • The receipt line is <nonce> <predecessor> writing|refused and a newline, 82 bytes; install.sh makes it holding the all-zero UUID twice and refused. A 45-byte receipt from bd7db43 stops install.sh with nothing changed and a message to remove it by hand.
    • The receipt is also the command guard. The root command opens it read-only on fd 8 and takes /usr/bin/lockf -s -t 10 8 before any other check (exit 75 if it stays locked), and its shell and pmset keep fd 8 until they exit. The app (SleepOffReceipts.lock), backstop.sh, uninstall.sh (lock_receipt) and install.sh take the same lock and check that the path still names the locked file before they claim, read, write the release file or remove it. A busy or failed lock decides nothing.
    • Claim. <uid>.released beside the receipt is the user's own file (0600, 42 bytes, <nonce> free|held), made by install.sh. A start claims the receipt under its lock only while that file shows the receipt's nonce free, journals sleepOffAttempt (now with the predecessor and expires), then writes <nonce> held. Every settlement gives the claim back. Start in another folder is refused while a claim is unsettled.
    • The root command takes seven arguments: $6 is the predecessor nonce, $7 the receipt's device:inode at the claim. fd 8 and the path must both be $7 (7). After the sudo checks, the two nonces must be different uppercase UUIDs and the start's nonce not all zeros (7), and the receipt must still begin with $6 (exit 8, a new .refused status). Then root reads pmset -g (6) and writes the line with one fixed perl program run through /usr/bin/env -i /usr/bin/perl: sysopen 257 (O_WRONLY|O_NOFOLLOW, no create or truncate), the same device:inode as fd 8, one full syswrite, fcntl F_FULLFSYNC (51), close, reopen and read back. Any failure, or no perl, writes refused and exits 7 before pmset.
    • expires is the session's end or 130 s after the marker is written (AdministratorPrompt.answerWindow: the 120 s dialog, 3 s grace, 7 s launch), whichever is first. It is $3 and is journaled.
    • Verdict (SleepOffReceipts.verdict). Never wrote: this nonce with refused, another start's line naming the same predecessor, or the predecessor itself once the dialog ended by itself (AdministratorPromptError.dialogOver) or expires has passed. May have written, undone like an end at once: this nonce with writing, or a later line naming another predecessor. After expires, a missing, replaced, unsafe or malformed receipt is "may have" too; before it, and at any time for a lock that stays busy or fails, the verdict is undecided. Undecided keeps the attempt, the claim and sleepDisabledByUs, runs no pmset before expires unless owedBefore, and refuses Start. A timed-out or stuck dialog with at most 15 s left waits for expires before the read.
    • A settlement step that fails keeps the attempt and the claim with what the receipt showed. With never-wrote and no owedBefore it runs no pmset (attemptHold in the app, attempt_hold in backstop.sh) and Starts stay refused; uninstall.sh stops with no pmset.
    • backstop.sh (version 4, required by the app) and uninstall.sh call /bin/cat and /usr/bin/head through CAT and HEAD. uninstall.sh's settle_stop no longer aborts on an unset second argument, which bash 3.2's EXIT trap had turned into exit 0.
    • Docs: README (the files table, the receipt, the claim and lock, the outcomes, a new "No answer within 120 seconds" bullet, the limits and Uninstall), SECURITY.md, spec sections 1, 2 and 8, release-validation.md (changed rows and new Not run rows for the 82-byte receipt, the 45-byte stop, two folders, the held lock and perl), .greptile/rules.md, and .greptile/config.json rules 36, 48, 60 and 78.
  • Independent round 25 review (GPT-6.1-Sol, xhigh, needs changes at e741a7f), changed in 93fb3dc. R25-1 (P1): a settlement gave the shared claim back before it published its journal, so a crash between the two, followed by later starts of another folder whose refused lines replaced the one the settlement read, could turn an honest never-wrote start into "may have written" and clear another tool's 1. R25-2 (P1): with the receipt locked, the app and backstop.sh restored sleep once expires had passed or an earlier restore was owed, though a command already in pmset may still turn sleep off after that undo. R25-3 (P1, with Greptile 4220347392): install.sh read the release file before it took the receipt's lock, and replaced a held claim with free when it had just made the receipt. R25-4 (P2, the outside-diff Greptile comment 6046657261): prepare_low_power_off in backstop.sh, as Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 merged it, ran head through PATH. R25-5 (P1, the ownership finding, F7): open and unwaived; this round narrows one window (below, Not covered). R25-6 (P2): docs and comments promised more than the code proves.

    • Settlement order, in the app (publishSettlement, finishSettlement), backstop.sh (edit_state, finish_settlement) and uninstall.sh: remove the start's session.json (the one whose end is the start's deadline), then journal the decision while the claim is still held (sleepOffAttempt.settled true, with sleepDisabledByUs as it was before the start after "never wrote", or true after "may have written"), then give the claim back, then drop the record. A record already marked settled is only finished: no reader reads the receipt against it again, because once its claim went back a later start's lines may show something else. A failure or crash after the decision keeps the settled record, which holds nothing back: the undo follows the decision, Start is refused until the record is gone, and every later run, relaunch or uninstall tries to finish it. A crash or failure before the decision keeps the claim held, so the receipt still shows what it showed. A rolled-back start whose claim cannot be given back stays settled the same way. install.sh's recovery runs the staged bundle's backstop.sh, so it settles the same way. backstop.sh declares version 5 and the app requires it (BackstopVersion.required).
    • Undecided (the receipt stays locked, the lock fails, or the dialog can still be answered) now holds the sleep undo at any time: before or after expires, and whether or not an earlier session owes a restore (keepAttempt and attemptHold in the app, keep_attempt and attempt_hold in backstop.sh). The earlier restore stays owed in the journal and runs once a run gets the lock and the receipt settles the start. "Never wrote" with a failed settlement still holds it unless owedBefore; "may have written" holds nothing. Frozen processes, Low Power Mode and audio are still undone. uninstall.sh already stopped while a start is undecided; its message now also says a started command holds the receipt until pmset exits.
    • install.sh opens the receipt on fd 7 and takes its lock before it reads anything, then reads the receipt's line and the release file under that lock. A held claim in this user's 0600 file with one link and 42 bytes is kept, also right after install.sh made the receipt. A held claim in any other file stops the install with nothing replaced, because the app reads it as a claim. The receipt's own nonce free in a file of that shape is kept. Anything else (no file, another nonce free, other bytes, or the right bytes with another mode) is written new as the receipt's nonce free by sudo -n install -m 0600, which replaces the file by rename. Just before that write, install.sh checks again that fd 7 and the path are the same file, that the receipt and its folders pass the same checks, that the receipt still holds the line read under the lock, and that the release file's owner, mode, links, size, type and bytes are what it read (or that no file has appeared). The 45-byte receipt is still refused, not migrated. The sudoers rule is unchanged.
    • backstop.sh's prepare_low_power_off calls "$HEAD" (/usr/bin/head) for both -c 1 checks.
    • The root command reads pmset -g a second time after writing is written, flushed with F_FULLFSYNC and read back, and before its last clock check and pmset, unless $5 is 1 (the journal owns the bit, so neither read runs). A SleepDisabled 1, or a read that fails, writes refused over the record the same way and exits 6: "pmset -g, read again once this start's record was written, shows a SleepDisabled 1 this start did not set, or could not be read; it was left alone and sleep was not turned off". The early read stays, before writing (exit 6 as before). The clock check after the second read writes refused and exits 4, as before. The exit codes and the app's handling of each are unchanged.
    • Docs: README (the settlement order, the claim, the second read, the undecided hold and its cost, the wall-clock assumption, a command that never exits, the crash bullets), SECURITY.md, spec sections 1, 2 and 8, release-validation.md (three new Not run rows: a receipt held locked past the 130 s window, a release file made immutable during a dialog, and install.sh run again while another folder's dialog is up), .greptile/rules.md, the .greptile/config.json rules on lines 36 and 78, and the source comments in SleepOffReceipts.swift, AdministratorPrompt.swift, SessionManager.swift, backstop.sh, uninstall.sh and install.sh.
  • Independent round 27 review (GPT-6.1-Sol, xhigh, needs changes at 93fb3dc), changed in d933b68. R27-1 (P1, F7, ownership): open and unwaived; this round narrows A3 for the app that received the refusal (below, Not covered). R27-2 (P1): uninstall.sh removed the shared sudoers rule before it took the receipt's lock and found another Insomnia folder's held claim. R27-3 (P1): the 0644 receipt let any local account hold its advisory lock and so hold an owed sleep undo. R27-4 (P2): a settled successful start whose claim or record cleanup failed ended its healthy session on relaunch. R27-5 (P2): recovery reads of the journal and session in backstop.sh and uninstall.sh still ran head, cat and tr through PATH. R27-6 (P2): source messages overstated what deleting the marker, the answer window and writing prove. R27-7 (P1): supervise in install.sh and uninstall.sh died on a process-group SIGTERM or SIGHUP, or on a failed status write under errexit, and let fd 9 go while sudo still ran. R27-8 (P1): uninstall.sh counted a journal read that failed as clean and could drop a kept brightness. R27-9 (P2): the hosted Swift job hit its 1200 s watchdog.

    • Uninstall's shared check (R27-2). After the journal check and before step 5 removes anything, uninstall.sh takes the receipt's lock on fd 7 (lockf -s -t 10, after its own recovery lock, the order every reader uses) and reads the receipt and the release file under it. It stops with nothing removed when the release file shows held, names another nonce or cannot be read, when the receipt or its folders cannot be locked or read, or when the release file is not a regular file; the LaunchAgent, the sudoers rule, the app, the receipt and the journal stay. A receipt no start can claim (it fails the root command's checks) does not block. The lock stays held through the removals: before the rule goes, and again before the receipt and release file go, uninstall.sh checks that fd 7 is still the path's file, that both files read the same and that the folders still pass. The lock goes only after the bundle is removed.
    • The receipt's mode and entry (R27-3). install.sh creates the receipt 0600 (sudo -n install -m 0600 -o root -g wheel) and adds one ACL entry with sudo -n /bin/chmod +a "user:<name> allow read", the name from id -un and checked with id -u. A receipt an earlier build made (root's, one link, 82 bytes, no group or other write, no entry or only that one) gets mode 0600 and the entry in place, bytes, inode and any held claim kept. Any other entry stops the install. The app (SleepOffReceipts, through acl(3) and mbr_uuid_to_id), the root command, backstop.sh, uninstall.sh and install.sh accept the receipt only at mode 0600 with exactly that entry: allow, the read right alone, the user's uid, not inherited, no flags. The shell copies read the list with /bin/ls -le through one awk program, the same text in all five copies. Folders keep the no-allowing-ACL check. The root command's exit code is unchanged (7).
    • Healthy settled sessions (R27-4). Reconcile resumes the session of a settled start whose sleep entry is still journaled when that session is the start's own (its first end matches the start's deadline within 1 s). It arms the agent, reads that sleep is still off and keeps the deadline and lid behavior. Starts stay refused until a later transaction gives the claim back and removes the record. An unsettled record, a settled record beside another session, and a marker with no journaled attempt still end the session.
    • Fixed tools (R27-5). backstop.sh gets TR=/usr/bin/tr; uninstall.sh gets TR and CMP. The journal and session shape checks, the session read, the post-undo journal check and the diagnostic excerpt use "$HEAD", "$CAT" and "$TR". The status-file PID reads in install.sh and uninstall.sh use "$CAT" and stay diagnostic only. The journal shape checks no longer pipe plutil into head.
    • Messages (R27-6). The app and both scripts now say that writing means the command was about to turn sleep off and may have; that deleting the marker stops a command the dialog starts from then on, unless this user writes the marker again or the clock is set back; and that a voided start is settled once its answer window has ended and the receipt can be locked, staying unsettled with Starts refused and sleep left as it is while a command holds that lock. answerWindow's doc gives the window as a minimum. release-validation.md's lock-timeout rows say a stalled sudo gets SIGTERM only and the run keeps the recovery lock until sudo has exited and been reaped.
    • The bounded call's supervisor (R27-7). supervise, still byte-identical in install.sh and uninstall.sh, now turns errexit off and ignores TERM and HUP for the call's whole life, and starts the call as ( trap - TERM HUP; exec "$@" ). sudo is still never sent SIGKILL, and fd 9 goes only after wait.
    • Journal reads in uninstall.sh (R27-8). record_text_problems, the same in both scripts, returns 2 when the file cannot be read. uninstall.sh reads through plutil_read: a missing key and a null value are absent; any other failure is listed in READ_FAILURES and returns 2. journal_problems copies state.json into the run's private folder with a bounded cp and reads only the copy. The caller checks every reader's status and READ_FAILURES, and any failure stops the uninstall before anything is removed. state.json is removed only while cmp finds it equal to the checked copy. The settlement stops on a failed read before it takes the receipt's lock. backstop.sh's raw reader and conversion return 2 too, and a journal it cannot read whole is left as unknown.
    • The retained refusal (F7). When the app gets exit 6 but cannot journal the rollback, it keeps the start's nonce in refusedNonce, and every later settlement of that nonce in the same process keeps "never wrote" whatever the receipt shows.
    • CI (R27-9). AppEncodedJournalScriptTests runs its script rows two at a time, and the fake sudo's hang_on_term is ready only after its trap and runs no command substitution after it (below, Tests). No workflow, watchdog, partition or rerun change; the two-job split stays the parent's choice.
    • Docs: README, SECURITY.md, spec, release-validation.md (three new Not run rows for the receipt's mode and entry), .greptile/rules.md and the .greptile/config.json rules on lines 36, 48 and 78.
  • Independent round 29 review (GPT-6.1-Sol, xhigh, needs changes at d933b68), changed in 468db03, 1468950, 7e233e8, 29949a6 and f643d49. Finding 1 (F7) stays open and unwaived; finding 3 is reported, not fixed.

    • Root command (finding 1), both copies: a refused line that cannot be written is tried again under the receipt's lock, at most three tries within 3 s.
    • uninstall.sh (findings 2 and 4) stops before the bootout on any read it cannot finish, a busy lock, a held claim, another nonce, one shared file without the other, or a file or folder that fails its checks. Then sudo -v, sudo -n -v, and every root command through bounded sudo -n (30 s, SIGTERM only); a call that fails or times out stops it, and one still running keeps both locks.
    • install.sh (finding 5) repairs an earlier receipt only while the release file shows no claim, under the receipt's lock when the user can open it, the entry before the mode.
    • The app's ACL reader (finding 6) refuses a list when an acl(3) call fails or gives an undocumented answer.
    • backstop.sh (findings 7 and 8) reads private copies with a time limit; an entry, key, mode, session end or receipt head it cannot read is kept or is no proof, never absent.
    • Sessions (findings 9 and 10): a session.json with no finite first end the store can read is moved aside at launch; a settled start's own session resumes after clamped extensions, with the menu's cleanup line.
    • Finding 11: fixed sed and grep, no sudo pid printed. Finding 12: README, SECURITY.md, spec, release-validation.md, the .greptile/config.json install rule. Finding 13: a fixture run whose output cannot be captured throws.
    • Greptile 4228725521 (29949a6): on a full disk backstop.sh keeps its copies in memory, with the same checks, so a recorded restore still runs; a failed publish keeps the journal.
    • Greptile 4228725533 (29949a6): before removing the receipt, uninstall records the release file's identity, change time and line in .uninstall-receipt-removal; a rerun removes a lone release file only on an exact match with free.
    • Hosted failure on 7e233e8 (testBackstopKeepsAnOwnerACLAndStillUndoesTheJournal, 0 entries for 1): 468db03 published from the private copy, without the entry. 29949a6 publishes from a cp of the live journal, as main does, only if it holds the bytes the run read.

Tests

  • swift build: ok.

  • swift test (full suite, alone, under the shared lock): 504 tests, 0 failures, 0 skipped, 107 s, before the review fixes. After the first review fixes: 461 tests, 0 failures, 0 skipped, 97 s. After merging main (5330c28): 482 tests, 0 failures, 0 skipped, 102 s. After the install-order fix (0087d0a): 482 tests, 0 failures, 0 skipped, 100 s. After the password-first fix (71253d7): 489 tests, 0 failures, 0 skipped, 99 s. After the Codex fixes and merging main (b5f6de9): 515 tests, 0 failures, 0 skipped, 122 s. After the second review fixes (b180529): 535 tests, 0 failures, 0 skipped, 142 s (the run before the RM change: 534 tests, 0 failures, 155 s). After the third review fixes and merging main (887bc71): 630 tests, 0 failures, 0 skipped, 145 s. After the fourth review fixes and merging main (e1e56b0; the two commits after it change docs only): 706 tests, 0 failures, 0 skipped, 261 s. After the fifth review fixes and merging main (12c8e40): 776 tests, 0 failures, 0 skipped, 230 s. The same suite passed at ca56ec0, before Browser: ask before quitting, and never relaunch a browser that has not quit #30 landed: 749 tests, 0 failures, 0 skipped, 217 s. Two full runs after the first merge hung in Tests: wait for an end to finish, not start, before checking what it restored #44's testEndDuringReconcileMustNotLeaveSleepDisabled, which waited for a reconcile disablesleep 1 this branch never runs (fixed in b01a97b). One more hung in RecoveryLockTests in waitUntilExit, the Foundation bug Process: wait for children with an exit handler, not waitUntilExit #46 fixes, and its rerun passed 625 of 625 at b01a97b before the second merge. The run before it failed one test this PR does not change, testBackstopSharesLockHandedDownOnFd9, whose fake sudo did not start within the fixture's 1 s command timeout; it passed 10 of 10 runs alone and in the rerun. Every run since the first review fixes used --skip UIStatusTests --skip UIStartupTests (those two suites put real status items in the maintainer's menu bar; hosted CI runs them on every push). One earlier full run under heavy load from parallel workers (load average above 30) failed 5 tests: this PR's SIGTERM timeout test, whose fake child was signalled before its TERM trap was installed (the test now uses a 3 s deadline), and four untouched RecoveryScriptTests with 1 s fixture timeouts (testLegacyAndIdentitylessPidsAreNeverSignaledAndStayDirty, testLiveSupervisorDoesNotHoldACallersCapturePipe, testUninstallAbortsWhenBootoutAndPrintBothFailAmbiguously, testUninstallRunsRecoveryUnderItsOwnLockAndKeepsLockInode), which pass alone and in every other full run.

  • swift build -c release -Xswiftc -warnings-as-errors (what CI now runs): ok, also at b5f6de9, b180529, 887bc71, f72ba11 and 12c8e40. /bin/bash -n on every script under /bin/bash 3.2.57 and the CI bash 4 grep: clean, also at b180529, 887bc71, f72ba11 and 12c8e40.

  • shellcheck scripts/*.sh: clean, also at b180529, 887bc71, f72ba11 and 12c8e40.

  • Full suite, sixth round, under the shared lock with --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests. From this round on KeychainStoreTests is skipped locally, because it calls the real Security APIs on temporary keychain files; hosted CI still runs it and both UI suites. The local full runs listed above from before 23d625f did not skip it, so they were not entirely on fakes.

  • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: ok at edfb2d7 and fa281c1.

  • /bin/bash -n on every script under /bin/bash 3.2.57, the CI bash 4 grep and shellcheck scripts/*.sh (0.10.0): clean at 23d625f and 6c02da5 (fa281c1 changes no script). actionlint and zizmor are not installed on this Mac; .github/workflows is unchanged from main, and hosted CI runs both.

  • Upgrade fix for 6046657261 (2b8028c):

    • A focused run at 8d3fe57, after the Release: build-app.sh, verified --app installs, and a release workflow #33 merge and the deadline fix: OsascriptAdministratorPromptTests, RootCommandTests and the 14 install tests Release: build-app.sh, verified --app installs, and a release workflow #33 changed, picked by an anchored filter and listed before the run. 51 tests, 0 failures.
    • The seven new and changed upgrade tests: 0 failures, 36.6 s. Control run: with --own-bundle taken out of install.sh's call, both upgrade tests that expect success failed (6 assertions, install exit 1). install.sh was then put back from a saved copy and compared with cmp.
    • Script and recovery suites (RecoveryScriptTests, PackagingTests, ReleaseWorkflowTests, BackstopVersionTests, LaunchdBackstopTests, ResumeFrozenCommandTests, RecoverySafetyTests, LidSimulationGateScriptTests), picked by an anchored filter with the three skips; the 332 tests were listed before the run: 332 tests, 0 failures, 644 s, on the scripts and tests committed in 2b8028c.
    • Full suite, /usr/bin/lockf /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1125 tests, 0 failures, 0 skipped, 747 s, at 2b8028c.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: ok on the tree committed as 2b8028c (Sources are unchanged since 8d3fe57).
    • /bin/bash -n on every script under /bin/bash 3.2.57, the CI bash 4 grep and shellcheck scripts/*.sh (0.10.0): clean. .github/workflows is unchanged; actionlint and zizmor are not installed on this Mac.
  • Round 16 (the Backstop: the supervisor owns each undo command's limit and signal #50 merge and the round 14 P1), all on fakes. Each focused command used an anchored filter, listed the selected names before the run, passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests and was checked afterwards against the names that ran.

    • Focused run: OsascriptAdministratorPromptTests, RootCommandTests, PendingStartRemovalTests, PmsetSleepGuardPromptTests, SleepPromptLifecycleTests, StartOwnershipEndToEndTests and the two RecoveryScriptTests cases above. 118 selected, 118 run, 0 failures, 57.9 s.
    • Full suite at 42e42e5, /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1161 tests, 0 failures, 0 skipped, 760.4 s, exit 0. All 1161 names that ran passed, none from the three skipped classes. pmset -g log shows no sleep during the run.
    • The round 15 full run, on 70f5fac with that round's uncommitted work and before the P1 change, ran 1153 tests in 1794.8 s with 2 failing cases (4 assertions). testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked expected the old pmset order (fixed above). testInstallLeavesAVisudoThatIgnoresSigtermHoldingTheLockAndTheRuleUntouched took 197.4 s, and its fake sudo ended by its own 60 s watchdog ("watchdog", not "released"). pmset -g log shows a 192 s Clamshell Sleep from 17:11:41 to a DarkWake at 17:14:53, and the case ran from about 17:11:37 to 17:14:54. The fake's watchdog uses date and install.sh's 5 s limit and TERM grace use bash's SECONDS, both wall clocks, so all three ran out during the sleep. The assertions that check the run itself passed in that run: fd 9 open, SIGTERM logged, the "still running as pid" message with its sudo kill line, "Nothing was changed", and no install, -l or launchctl call. Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43's review found a fixture race where a group signal kills the fake's date and the fake exits at once. That race would have made install.sh report a stop on SIGTERM, which this run did not. The test is unchanged. An anchored rerun of it passed in 12.8 s.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: ok. /bin/bash -n on every script under /bin/bash 3.2.57, the CI bash 4 grep, shellcheck scripts/*.sh (0.10.0) and git diff --check: clean. No script changed in round 16.
  • Round 18 (the round 17 findings), all on fakes. No real sudo, pmset or osascript ran, no dialog was shown, and the installed sudo was never run; its version, 1.9.17p2 on macOS 26.2, comes from the installed man pages. Each focused command used an anchored filter, listed the selected names before the run, passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests and was checked afterwards against the names that ran.

    • Focused run on the tree committed as 576c215: RootCommandTests, RootCommandSudoAnswerTests, OsascriptAdministratorPromptTests, SleepPromptLifecycleTests, StartOwnershipEndToEndTests, PmsetSleepGuardPromptTests, PendingStartRemovalTests and testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked. 118 selected, 118 run, 0 failures, 119.0 s. An earlier run of the same set failed 2 end-to-end tests: the new fake sudo looked the app's restore up under the fake pmset's path, while the app always runs /usr/bin/pmset. The fake now maps that path; no assertion changed.
    • Full suite on the tree committed as 576c215 (7dcf51f, committed during the run, changes only .greptile/config.json; source fingerprint c44f310027475abc before and after), /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1162 tests, 0 failures, 0 skipped, 803.0 s, exit 0 (2026-10-08T03:28:23Z to 03:42:12Z). swift test list gave 1229 names; without the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests, 1162 were selected, and the 1162 executed are the same names. None from the three classes ran. The shared lock file kept inode 201732085.
    • Controls, the old command (42e42e5's rootCommand) against the new one, run as the user under the real lockf with a fake sudo, pmset and clock: 27 rows, all as expected. R1: with no rule, the old command runs root's disablesleep 1 before the user's sudo, then its fallback 0; the new one runs no pmset. R3: with the clock reaching the deadline, or 1 s or a day past it, during root's second pmset -g, the old command exits 0 and leaves 1; the new one exits 4 with no write when the clock gets there during sudo -V, -l, -ll or pmset -g. R2: with no rule and another tool's 1 set during the check, the old command exits 5 and leaves 0, or 70 and 0 with stderr closed; the new one exits 5 and leaves the 1 either way. With the rule and a 1 set during -ll, the new one exits 6 and leaves it, also with stderr closed.
    • The round 18 tests on 42e42e5's Sources, in a throwaway copy: the tests for the three findings fail there. testARefusalKeepsItsStatusWhenTheDialogsOutputIsGone gets 70 instead of 5 and pmset -g, -a disablesleep 1, -a disablesleep 0 instead of nothing; testWritesNothingWhenTheDeadlineComesDuringAnyCallBeforeTheWrite, testAnEndDuringRootsReadWritesNothing and testASettingMadeWhileSudoIsAskedSurvivesARefusal see the old writes. Most of the 810 failed assertions there are structural: the fakes count env and date calls the old command never makes, hooks keyed to the new queries never fire, and RootCommandSudoAnswerTests fails its count of the command's awk programs (one in the old command, four in the new) and stops there. The harness rows above are the precise controls.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: ok. /bin/bash -n on each script under /bin/bash 3.2.57, the CI bash 4 grep, shellcheck scripts/*.sh (0.10.0) and git diff --check: clean on 576c215. actionlint and zizmor are not installed on this Mac; .github/workflows is unchanged.
    • No mutation run this round.
  • Round 20 (the round 19 findings), all on fakes. No real sudo, pmset or osascript ran, no dialog was shown, no plugin was built or loaded, and no real sudoers, sudo.conf or PAM file was read. The tests put private fixture files in place of /private/etc/sudo.conf and /private/etc/pam.d/sudo and check that the command names each path exactly once. Apple's sudo-114.100.11 source was downloaded and read, not built; its hashes and the cited lines are in the evidence manifest. Focused commands used anchored filters, listed the selected names first, passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests and were checked against the names that ran.

    • Focused run on the tree committed as c5456f8: round 18's set plus ReconcileTests, RecoverySafetyTests and testBackstopVoidsTheDialogOfAStartThatDiedBeforeItUndoesAnything. 199 selected, 199 run, 0 failures, 143.6 s.
    • Full suite on the tree committed as c5456f8 (clean tree, source fingerprint 6d25e797b6e93cd4 before and after), /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1179 tests, 0 failures, 0 skipped, 857.8 s, exit 0. 1179 selected (1246 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests) and the same 1179 run; none of those three classes ran. Against round 18's 1162, 3 names were removed and 20 added.
    • Controls: 7dcf51f's rootCommand in the same end-to-end fixtures (scratch tests, not committed). With the silent approval plugin's sudo.conf, and with the review's user Defaults, it turned sleep off (-g, -g, -a disablesleep 1), and the end's restore then failed with status 1, leaving SleepDisabled 1 and the journal entry. The new command refuses both before any pmset (testASudoConfStopsTheStartBeforeSudoIsAsked, testUserDefaultsTheCheckDoesNotAcceptStopTheStart).
    • Two hand mutations of the marker rule, each put back afterwards with its hash checked. .untouched never returned: 5 of the 8 F3 tests fail. The file not compared, content only: only the unit test failed, so the lifecycle tests did not cover a replaced marker. testAFailureWithAReplacedMarkerIsUndoneEvenWhenItHoldsTheNonce and testAFailureWithNoMarkerLeftIsUndone were added, and the first fails under that mutation.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: exit 0 on c5456f8 (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1). /bin/bash -n on each script under /bin/bash 3.2.57, the CI bash 4 grep, shellcheck scripts/*.sh (0.10.0), git diff --check and osacompile of the dialog script (compile only): clean. No script changed this round.
  • Round 22 (the round 21 findings), all on fakes. No real sudo, pmset, osascript, install.sh or uninstall.sh ran, nothing was written under /private/var/db, no dialog was shown, and no system sudoers, sudo.conf, PAM or account file was read. Every receipt is a file in a test's temporary folder. Focused commands used anchored filters, listed the selected names first, passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests and were checked against the names that started and ended.

    • Focused runs before the commit. Each ran the product code and scripts committed as bd7db43, except the first prompt run (below); tests changed after two of them, as described below. The prompt, root command, receipt and settlement classes (OsascriptAdministratorPromptTests, RootCommandTests, RootCommandSudoAnswerTests, PendingStartRemovalTests, PmsetSleepGuardPromptTests, SleepPromptLifecycleTests, StartOwnershipEndToEndTests, BackstopVersionTests, SleepOffReceiptsTests, SleepOffSettlementTests): 162 selected, 162 run, 0 failures, 102 s. RecoveryScriptTests: 265 selected, 265 run, 263 passed and 2 failed (below), 731 s of tests (the run waited 16 minutes for another worktree's full suite to release the shared lock). Then the session, store and recovery classes (ReconcileTests, ReconcileLidGatingTests, RecoverySafetyTests, StillRunningCommandTests, StoreTests, UndoClearFailureTests, IntegrationWiringTests, AppNapTests, LaunchdBackstopTests, PrivilegedCommandLockTests, CommandCancellationTests, RecoveryLockTests), both receipt classes again (their two child processes now wait through ProcessExit, as Process: wait for children with an exit handler, not waitUntilExit #46 asks, not waitUntilExit()), and the three changed install tests: 239 selected, 239 run, 0 failures, 53 s.
    • Earlier focused runs that failed, kept in the evidence. The first prompt run failed 7 of 142, all in the tests: RootCommandTests built a fresh test receipt for each run when it was not given one, which put back the file and modes a test had just made unsafe (the receipt checks were right; the tests now pass their receipt); a PmsetSleepGuardPromptTests prompt had no receipt, so its command exited 7; the awk count guard in RootCommandSudoAnswerTests expected 5 programs where the command now has 6; and an end-to-end expectation left out the app's undo. That run also came before the last two product changes: the app's removal of a session.json beside a marker no journaled start accounts for, and backstop.sh's undo and exit 1 when a settlement or that removal cannot be published. The passing rerun of the same classes and the full suite include them. A receipt and settlement run failed 1 of 20: the new settlement-failure test expected the journal refusal, but with the folder read-only the end of the unsettled session was also pending, and Start's pending-end refusal came first. That test now expects a refusal there and adds an immutable state.json case, which reaches the journal refusal. The RecoveryScriptTests run failed 2 older install tests, testInstallWritesExactlyThreePasswordlessLinesAndNoneTurnsSleepOff and testReinstallOverFourLineRuleLeavesThreeLines: they counted every sudo -n /usr/bin/install call as the sudoers file's, and install.sh now makes a second one, for the receipt. They now require exactly one install of the sudoers file, as before, and the receipt's as the only other. testInstallWithNoAppRunningStopsBeforeTheBundleWhenSudoersRuleIsNotEffective, which looked for any install call, now names the sudoers file and checks that nothing touched the receipt before the rule's check. All three ran again in the next focused run.
    • Full suite on the tree committed as bd7db43 (clean tree, source fingerprint 4760c084715aa73e before and after), /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1223 tests, 0 failures, 0 skipped, 888.0 s, exit 0 (09:30:52Z to 09:45:41Z on 2026-10-08). 1223 selected (1290 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), the same 1223 started and ended, none from those three classes. Against round 20's 1179: 5 removed, 49 added. The lock file kept inode 201732085 before, while lockf held it (lsof of lockf's descriptor) and after.
    • The review's four probes, rewritten as safety tests on the new code. Its matched control (a failure after the write is undone) is testAFailureAfterTheWriteIsUndone. The same-inode forgery is testAForgedMarkerDoesNotDropTheRestore: the marker holds the bare nonce again in the same inode, and the start is still undone, with pmset -g, -g, -a disablesleep 1, -a disablesleep 0, a clean journal and no "never turned sleep off" notice. The two relaunch probes are testARelaunchDoesNotResumeAnUnexpiredStartOnAnotherToolsSetting and testARelaunchDoesNotRestoreAnExpiredStartThatNeverTurnedSleepOff. Each asserts what should now happen, where the probes asserted the unsafe outcome.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: exit 0 on bd7db43, a clean tree (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1). /bin/bash -n on each script under /bin/bash 3.2.57, the CI bash 4 grep, shellcheck scripts/*.sh (0.10.0), git diff --check and osacompile of the dialog script (compile only): clean on bd7db43, which changes backstop.sh, install.sh and uninstall.sh. git diff --check was clean from c5456f8 and from main. osacompile ran inside testScriptCompiles in the full suite, which passed and did not skip. .github/workflows is unchanged from main; actionlint and zizmor are not installed on this Mac.
    • No mutation run this round.
  • Round 24 (the round 23 findings and the Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 merge), all on fakes. No real sudo, pmset, osascript, perl as root, install.sh or uninstall.sh runs, and no test writes under /private/var/db. The fake perl records each line and can fail each step of the write (open, short write, F_FULLFSYNC, close, read-back) or be missing. Each focused command used an anchored filter, listed the selected names before the run, passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests and was checked afterwards against the names that started and ended.

    • New: 18 in SleepOffReceiptsTests (the claim, the receipt lock, the verdict over line, predecessor and time, dialogOver, two starts claiming one line, a never-write that cannot be published, a missing receipt kept recorded), 12 in AdministratorPromptTests (the receipt lock and exit 75, the device:inode check, the predecessor check and exit 8, every perl fault, the 130 s window and the 15 s wait, a signal the runner did not send) and 20 in RecoveryScriptTests (backstop.sh and uninstall.sh: the lock, the claim, a dialog that can still be answered, a later line, a replaced receipt, a never-write that cannot be published, the owed restore, and CAT/HEAD with a hostile cat and head first on PATH). 16 tests whose names stated the round 23 rules were replaced.
    • Development runs on the uncommitted round 24 tree (head bd7db43), kept in the evidence:
      • d1: 32 tests, 2 failed (3 assertions). Both expected the attempt hold's own refusal text, but with the home read-only the pending end's refusal comes first; the assertions now require a refusal, not its wording. d1b: 32, 0 failed.
      • d2: 279 tests, 3 failed (5 assertions), 763 s. In one, the test's check for no sudo /bin/rm also matched the expected sudo /bin/rmdir; the test now looks for sudo /bin/rm -f. Two were uninstall.sh reading an unset $2 in settle_stop; bash 3.2's EXIT trap turned that abort into exit 0. Fixed in the script. d3: those 3, 0 failed.
      • d3b: 142 tests, 25 failed (100 assertions), all expectations of the round 23 command. d4: 145 tests, 2 failed (a later start's dialog inherited a test hook, and a call list missed the transaction's own pmset -g). d5: those 4, 0 failed.
    • The first build after the merge failed (PrivateDisplayGuardTests); my wrapper printed the failure but returned 0, and I first read it as a pass. Fixed in 15c2fc3 and rebuilt: exit 0.
    • d6, on the merge 15c2fc3 (fingerprint ba108891d50179b3), the round 24 classes with every class Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 changed: 836 selected, started and ended; 833 passed and 3 failed (5 assertions), exit 1, 1220.8 s (13:46:17Z to 14:06:39Z). All three were in EarlierBootLowPowerClaimTests, which seeded a still-valid session without SleepDisabled 1; changed in e741a7f (Merged main).
    • d7, on e741a7f's tree before the commit (fingerprint ffd53d415a63c777, the same as e741a7f): EarlierBootLowPowerClaimTests, LidCloseCopyTests and ReleaseWorkflowTests, 24 tests, 0 failures, exit 0.
    • Full suite on e741a7f, /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1402 tests, 0 failures, 0 skipped, 1221.3 s, exit 0 (14:09:20Z to 14:29:42Z on 2026-10-08). 1402 selected (1469 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), 1402 started and ended, no duplicates and none of the three classes run. Clean tree, source fingerprint ffd53d415a63c777 before and after; swift test was lockf's direct child, and the lock file kept its inode. Other sessions' focused runs used the machine during this run outside the shared lock; they were not stopped. The 1223 passes on bd7db43 are proof for that tree only.
    • swift build -c release -Xswiftc -warnings-as-errors, scripts/check-lid-simulation-gate.sh, /bin/bash -n on every script under /bin/bash 3.2.57, shellcheck scripts/*.sh (0.10.0), git diff --check and a JSON parse of .greptile/config.json: exit 0 on e741a7f, a clean tree, and no findings from the checks.
    • No mutation run this round.
  • Round 26 (the round 25 findings), all on fakes. No real sudo, pmset, osascript, perl as root, install.sh or uninstall.sh runs, and no test writes under /private/var/db. Each focused command used an anchored filter, listed the selected names before the run, passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests and was checked afterwards against the names that started and ended; none of those three classes ran.

    • New, 20 tests. SleepOffSettlementTests (7): a crash after the decision was journaled, with the claim still held or already given back, followed by zero, one or two later refused starts of another folder, for never-wrote with nothing owed, never-wrote behind an owed restore, and may-have-written; a claim that cannot be given back, for never-wrote and may-have-written; a settled record that cannot be removed; a rolled-back start whose claim cannot be given back; a claim that cannot be written before the dialog; and a busy receipt across all four combinations of an expired or open window and an owed or clear restore, with Low Power Mode, frozen processes and the volume still undone. RootCommandTests (4): another tool's 1 set while the record is written and flushed, and the second read failing, through both copies of the command; a refused over the record that cannot be written after the second read (the limit, below); a journal-owned 1 that skips both reads; and the clock read again after the second read, at the deadline's second and one before. RecoveryScriptTests (9): backstop.sh finishing a settled record (claim held or given back, behind one or two later lines) and uninstall.sh finishing one behind two later lines; backstop.sh keeping a settled record it cannot finish (release file not as install.sh made it, immutable state.json, receipt locked) with nothing held back; backstop.sh holding the sleep undo while the receipt is locked across the four window and owed combinations, and while the dialog can still be answered with a restore owed; prepare_low_power_off with a hostile head first on PATH beside the ordinary PATH; and install.sh keeping a claim taken while it waited for the lock (receipt made just now and made before), keeping a claim right after it made the receipt, stopping at a claim in an unsafe file, rewriting a release file with no claim, and stopping when the release file changes between the read and the write.
    • Changed expectations. testBackstopRunsTheRestoreAnEarlierSessionOwesWhileTheDialogCanStillBeAnswered is now testBackstopHoldsEvenAnOwedRestoreWhileTheDialogCanStillBeAnswered. testBackstopSettlesAStartOnlyOnceTheCommandBehindItsDialogIsDone, testALockedReceiptKeepsTheStartRecordedUntilItIsLetGo and testARelaunchKeepsTheRestoreAnEarlierSessionOwes expected the early undo R25-2 removes; they now expect no restore while the command may act and the restore after the lock is let go. Every root command call list gains the second -g, run as root. The tests that show another tool's 1 being cleared (testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite, testASettingMadeRightAfterTheCommandsReadIsClearedOnlyWhenTheWriteFollows, testWhereAnotherToolsOneLandsDecidesWhetherItSurvives) now set that 1 after the second read; they describe F7 and are not guards.
    • Development runs on the uncommitted tree (head e741a7f), kept in the evidence:
      • d1: my runner's discovery used swift test --skip-build --list-tests, which gave it no list, and the runner went on with an empty selection instead of stopping; the filter, anchored only at its start, ran 37 tests of BackstopVersionTests, SleepOffReceiptsTests and SleepOffSettlementTests (none of the three skipped classes), 4 failed (7 assertions), all expectations of the early undo or the single -g. Discovery then moved to swift test list --skip-build with the three skips; it still lists those classes, and the selection drops them.
      • d2: 189 tests, 19 failed (50 assertions), 125 s, all in the tests: the second -g and its third root in the call lists; the F7 description tests, whose 1 set after the first read the second read now caught (exit 6, the 1 stayed), so they now set it after the second read; the timed-out dialog's message for the R25-2 hold; and one assertion of a new test. d3, the same classes: 193 tests, 0 failed, 179.4 s.
      • d4: RecoveryScriptTests, 309 tests, 306 passed and 3 failed (14 assertions), 1083.5 s. testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked missed the second -g; testBackstopRestoresSleepAndKeepsTheStartWhenTheReceiptOrItsFolderCannotBeTrusted expected an unsettled record where a lower-case nonce case now leaves a settled one; both test fixes. testTheSupervisorOutlivesItsRunAndGroupSignalsAndHoldsTheLockUntilItReapsTheCommand failed once at 26 s (one SIGTERM, then its watchdog); this round changes no supervisor code, and it passed in d5 (6.5 s). d5: those 3 and the fixture probe, 4 tests, 0 failed.
      • d6: the locked-receipt hold test after its lock-wait change, 1 test, 0 failed, 8.9 s (18.5 s in d4). d7: LoginItemTests, LidCloseCopyTests, PrivateDisplayGuardTests, ReleaseWorkflowTests, BackstopVersionTests and SleepOffReceiptsTests after the docs, 63 tests, 0 failed.
    • CI cost (R25-CI). Fixture setup measured with a probe test (removed before the commit, three runs): ScriptFixture init about 0.023 s, the app journal about 0.005 s, teardown about 0.003 s, against 2.5 to 3.5 s per uninstall.sh run and 2.2 to 3.0 s per backstop.sh run. One traced backstop.sh run executed 1226 trace lines, 73 of them plutil, 14 fake date and 5 0.1 s polls: the cost is the scripts' own work, not the fixtures. Preparing fixtures once would save about 0.03 s per case (about 0.7 s for the 24-case matrix), so no fixture change was made. One change: the new locked-receipt test gives the backstop's lockf no wait (-t 0) while the test holds the lock, 18.5 s to 8.9 s, with the same assertions. The round adds 20 tests, about 50 s locally. The hosted job already exceeded its 1200 s watchdog on e741a7f, so it is at risk of doing so again; no timeout or matrix was changed.
    • Full suite on 93fb3dc, /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1421 tests, 1420 passed, 1 failed (6 assertions), 0 skipped, 1546.9 s, exit 1 (17:24:26Z to 17:50:13Z on 2026-10-08). 1421 selected (1488 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), 1421 started and ended, no duplicates and none of the three classes run. Clean tree, source fingerprint 15abc0f993dca810 before and after; swift test was lockf's direct child, and the lock file kept its inode. The failure is testValidKeptDisplayRecordsAreKeptForTheApp, a display-record test from main whose test code and fixture this round did not change: in two of its cases the fixture's 1 s command limit (COMMAND_TIMEOUT_SECONDS=1) ended the fake sudo pmset -a disablesleep 0 with SIGTERM, so backstop.sh kept the journal dirty and exited 1. It took 31.6 s against 19.2 s on e741a7f; the load average was about 5.6 with XprotectService near 50% CPU, and the Mac did not sleep. Rerun alone on 93fb3dc (same fingerprint, anchored filter, the three skips) once the shared lock was free: 1 test, passed, 20.6 s, exit 0; one failing run in the full and one passing run alone, not labelled a flake. This full is not a clean pass, and no second full was run.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: exit 0 on 93fb3dc, a clean tree (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1). /bin/bash -n on every script under /bin/bash 3.2.57, the CI bash 4 grep, shellcheck scripts/*.sh (0.10.0), git diff --check from e741a7f and from main, and a JSON parse of .greptile/config.json: no findings; .github is unchanged from main. actionlint and zizmor are not installed and did not run.
    • No mutation run this round.
  • Round 28 (the round 27 findings), all on fakes. No real sudo, pmset, osascript, chmod +a, ACL change, perl as root, install.sh or uninstall.sh runs, and no test writes under /private/var/db. Each focused command passed --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests, listed the catalog with those skips (Swift 6.3.1 still lists the three classes, so the runner drops them by name), selected exact anchored names, stopped on an empty or unmatched selection, and was checked afterwards against the names that started and ended; none of those three classes ran.

    • New, 34 tests. R27-2, uninstall's shared check (8, RecoveryScriptTests): another folder's writing and refused starts with a held claim, which that folder's backstop then settles through the kept rule before the uninstall finishes; a locked receipt; a damaged, other-nonce, unreadable or folder release file, a damaged receipt and a group-writable folder; a claim that appears while uninstall waits for the lock; a receipt replaced under the lock; a release file that changes after the check; a lock probe that shows the lock held at the bootout and at each sudo rm; and a receipt no start can claim, removed only while its release file shows no claim. They replace three tests that expected the shared rule gone after a refusal. R27-3, the receipt's mode and entry (8): the root command (both copies) refusing modes 644, 640, 400 and 700 and 23 other lists with exit 7 and no calls; backstop.sh trusting only mode 600 with that entry, and uninstall.sh treating any other mode or list as a receipt no start can claim; one awk program in all five copies, run against real id for this user and daemon; install.sh repairing a receipt it made before and stopping at one it cannot repair; the app's acl(3) reader accepting only that entry; and the Swift reader against ls -led of real folders that already carry ACLs (read-only, skipped when none is found). R27-4 (3, SleepOffSettlementTests): the settled start's own session resuming while its cleanup fails (claim held or already free, a locked receipt, a read-only or damaged release file, an immutable journal), then cleanup and a new Start; the same beside another folder's later claim; and only the start's own session resuming (extensions, the 899.6 s clamp, another session, 1 s and 2 s offsets, no sleep entry, sleep turned back on, expired). R27-5 (2): backstop.sh reading the session and publishing the journal through its own tools, and uninstall.sh reading a leftover session the same way; the three hostile-PATH tests now require no hostile call at all. R27-7 (3): the exact bounded and supervise text of each script with a fake sudo that closes fd 9 and sets its traps before it reports ready, in an owned process group: the main shell killed, then group TERM and HUP (two TERMs and one HUP reach the call, the lock stays held until "sudo exiting" with status 124); a failed status write under errexit; and the call's default signal actions and its stop at the limit. R27-8 (9): a copy made unreadable after its first conversions (the review's schedule, purge and not); five later read failures, including partial output then an error; one of two kept levels unreadable; null and missing keys read as absent; a journal that becomes a FIFO before it is copied; a journal that changes or appears after the check; a failed read for the settlement; the record reader returning 2 in both scripts; and backstop.sh leaving a journal it cannot read whole as unknown. F7 (1): testARefusalWhoseRollbackCannotBeJournaledStaysARefusalInThisProcess.
    • Matched controls, kept in the evidence. The R27-8 tests against the 93fb3dc scripts: 8 of 9 fail (the null and missing control passes on both). The R27-7 tests against the old supervise and against a mutant without the call's trap - TERM HUP: fail as expected. The F7 test with the retention removed: fails at the same-process undo; the source was put back by hash and rebuilt.
    • Development failures on the uncommitted tree, kept: f2-uninstall-1 (83 selected, 9 failed, 262.9 s): uninstall.sh read the receipt a second time through the lock's fd 7 and found it at end of file; it now reads the receipt by path (f2-uninstall-2, 9 of 9). f3-1 (173 selected, 3 failed) and f4-1 (73 selected, 1 failed): test errors, in how one test cut the awk program out of its $(...), a fixture helper that wrote the receipt 0600 with no entry, and the stand-in ACL fixture. f5-f8-new-3 (15 selected, 4 failed) and f5-f8-new-4 (1 failed): the new hostile-PATH tests caught a bare cat in the fakes and then in an uninstall.sh heredoc, now "$CAT". f8-new-2 (9 selected, 1 failed): a quoting error in the test's failure trigger. f5-f8-new-5 (15 selected, 1 failed, 687.8 s) and f8-fifo-rerun-1 (123.1 s): the FIFO test ran inside maintenance sleeps of 590 s and 122 s (read-only pmset -g log); it now measures awake time, and f8-fifo-rerun-2 passed. f9-concurrency-1 and -2: CI cost, below. f7f9-broad-1 (580 selected, 577 passed, 3 failed, 2316.1 s): testScriptsAndAppReadTheSameSessionDates, whose harness lacked uninstall.sh's new plutil_read (test fixed); testUninstallAbortsOnMalformedJournal, because this round had put a type read before the whole-journal conversion check, so a malformed journal got the "could not be read whole" message (uninstall.sh order fixed); and testUninstallTreatsAHungPgrepAsRunning at 170.2 s against 30 s, which ran 18:04:57 to 18:07:48, the span of a maintenance sleep. fix-uninstall-2 then passed 104 of 104 (329.4 s) and the adjacent classes 317 of 317. f7-supervisor-1, f7-supervisor-2 and fix-uninstall-1 stopped before running anything because a selector matched no test.
    • CI cost (R27-9). AppEncodedJournalScriptTests' three script matrices (agent, uninstall and the record reader) now build each row's home and environment one at a time, run the rows' scripts at most two at a time, and check each row one at a time afterwards; App Harness work, journal encoding and the global home stay serial. On the same binary in the same hour, the three methods took 108.4 s two at a time (f9-concurrency-3) against 132.4 s one at a time (f9-serial-control-1, a temporary patch): 24 s, or 18%, saved, about half the review's 52 s estimate. Two earlier two-at-a-time runs failed: f9-concurrency-1 during a clamshell sleep and a 743 s maintenance sleep, and f9-concurrency-2 in the uninstall matrix, where one row stopped after two pgrep -x Insomnia calls; an 11 s idle sleep and another session's swift test overlapped it, and the fixture's unchanged 1 s command limit under load is the likelier cause, not proven. Two uninstall rows at once add load against that limit. Fake tool startup is not where the time goes: the logging fake head costs 2.2 ms per call more than the real one and the fake sudo's $(cat) mode read 0.6 ms more than read, at about 10 to 12 fake calls per 3 to 5 s row. The fake sudo's hang_on_term now sets its deadline from SECONDS, sets its trap, and only then writes "ready" and its pid, with no command substitution after the trap. In a Bash 3.2 probe of the old loop with a slow date, a group SIGTERM sent during $(date +%s) ended the wait at once (5 of 5); with its sleep removed, one SIGTERM was logged twice in 73 of 200 trials. The new loop was released 5 of 5 and logged once in 200 of 200. Those match the two recorded supervisor-test failures (d4's early end and PR Install, uninstall: identify a running Insomnia by path or bundle id, not by name #18's extra TERM) in kind; the probe does not prove they were the cause. PR Backstop: end a valid session when the app is gone, the battery is below the end floor, or heat is critical #34 has a related fixture change; nothing from it was copied.
    • Full suite on d933b68, /usr/bin/lockf -k /private/tmp/insomnia-fable/swifttest.lock swift test --skip UIStatusTests --skip UIStartupTests --skip KeychainStoreTests: 1452 tests, 1452 passed, 0 failed, 0 skipped, 1579.4 s, exit 0 (suite 18:43:03 to 19:09:23 PDT on 2026-10-08, after about 15 minutes waiting for the lock behind another session's run). 1452 selected (1519 listed, less the 40 UIStatusTests, 13 UIStartupTests and 14 KeychainStoreTests), 1452 started and ended, no duplicates and none of the three classes run. Clean tree, source fingerprint b60c89ccae70897f before and after, and the lock file kept its inode. The 34 new tests took 122.7 s; RecoveryScriptTests took 1214.5 s (1166.1 s at 93fb3dc); the three two-at-a-time AppEncodedJournalScriptTests methods took 80.6 s (104.3 s serial at 93fb3dc, a different run under different load). One full, no rerun.
    • swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh: exit 0 on d933b68, a clean tree, no warnings (plain release build: 0 watcher symbols and neither lid string; lid simulation build: 33, 2 and 1). /bin/bash -n on all seven scripts under /bin/bash 3.2.57, the CI bash 4 grep, shellcheck scripts/*.sh (0.10.0), git diff --check from 93fb3dc and from main, and a JSON parse of .greptile/config.json: no findings; .github is unchanged from main. actionlint and zizmor are not installed and did not run.
  • Round 30, all on fakes (no real sudo, pmset, osascript, install.sh or uninstall.sh). Runs dropped the three classes by name, selected exact anchored IDs and were checked against what started and ended.

    • 34 new tests, 2 of them replacing removed ones; catalog 1551 (1519 at d933b68).
    • A real ACL change ran: f1-root (85 passed, 06:11 to 06:18Z) included RootCommandTests/testAMissingOrUnsafeReceiptStopsBeforeAnyQuestion while it still ran /bin/chmod +a and -N on its own temporary files, against this round's instructions; it now uses a fake ls. Main's ReconcileTests/testJournalReadableOnlyThroughAnOwnerACLIsStillRestored, RecoveryScriptTests/testBackstopKeepsAnOwnerACLAndStillUndoesTheJournal and RecoveryScriptTests/testInstallFromAPrebuiltAppDropsOtherAccountsWriteAccessButKeepsTheSignatureAndQuarantine still do that, so no run included them and the full command did not run.
    • r30-focusedA was cancelled during an idle sleep; my SIGTERM to its lockf left its swift test running without the shared lock for about 75 s until I stopped it. Not counted.
    • Broad run on 468db03: 1474 selected, 1419 passed, 55 failed. 51 were one regression of this round: the shared shape_of reader set the global problems, uninstall.sh's step 4 array, so every uninstall stopped there as incomplete. 1468950 fixes it and 4 moved expectations; 7e233e8 fixes a new test's expected paths. The 55 then passed.
    • Broad run on 7e233e8: 1474 selected, 1474 passed.
    • Hosted run 37911049950 (7e233e8): the ACL failure above, then the 1200 s watchdog (09:46:08Z) killed testUninstallStopsAHungDefaultsReadAndFinishes 7.8 s in (about 9.3 s normally). Build release and the lid gate were skipped.
    • Broad run on 29949a6: 1481 selected, 1479 passed, 2 failed, 2614 s. Both were fixtures this round broke (a harness without the new plutil_on, a fake perl with its reason on stderr); f643d49 fixes them and its 15-test focused run passed. A 980 s Mac sleep inside one passing RootCommandTests case explains the time. Nothing skipped, duplicated, missing or unfinished.
    • On f643d49: the CI syntax step (bash 3.2, bash -n, the bash 4 grep), shellcheck scripts/*.sh, swift build -c release -Xswiftc -warnings-as-errors and scripts/check-lid-simulation-gate.sh (cached: no Swift source changed since 468db03), git diff --check, a parse of .greptile/config.json: exit 0. .github unchanged.

New coverage:

  • OsascriptAdministratorPromptTests (fake osascript in a temp dir, never the real one): the script literal is exactly the expected text with rootCommand embedded, compiles under osacompile (compile only, nothing runs), and reaches osascript as -e <literal> <marker> <nonce>, with a marker path full of quotes and $(...) arriving intact; the root command's refusal comes back as .failed with its message; exit 0 succeeds; User canceled. (-128) is .cancelled; a wrong password is .failed with stderr kept; a missing executable is .launchFailed; at the deadline the child gets SIGTERM only, the runner waits 1.5 s for its TERM handler to finish (longer than the 1 s SIGKILL grace CancellableCommand would allow) and still reports .timedOut. Review fixes: a child that ignores SIGTERM is reported .stillRunning 3 s after the deadline with a live pid, is not killed (kill(pid, 0) succeeds), and its handle resolves only when it exits on its own; a child that dies on SIGTERM while a TERM-ignoring holder keeps its output open leaves a handle whose osascript has exited and which resolves only when the holder lets go. Codex fixes: the deadline starts only after the fake has written a ready file after installing its TERM trap (beforeDeadline hook), and every holder runs until the test releases it (60 s watchdog), so no test depends on a scheduling window; a new test releases osascript first and checks that osascriptAlive turns false while the handle is still running, then releases the holder.

  • RootCommandTests (new): the real rootCommand, run as the user under /bin/sh with AppleScript's quoted form of quoting and /usr/bin/pmset replaced by a recording fake. A matching nonce runs -a disablesleep 1 and exits 0; a newer start's nonce and an empty nonce run nothing and exit 3, a missing marker exits 69 from lockf; a path and nonce full of quotes, backticks and $(...) match and run nothing else. Second review round: the command runs under the real /usr/bin/lockf with the literal markerLock. testTheMarkerCannotBeRemovedWhilePmsetRuns holds the fake pmset and checks that removePendingStart times out with the marker in place, then succeeds once pmset is done; testAnAnswerThatWaitsOnARemovalRunsNothing starts the command while a remover holds the lock and checks it runs no pmset after the removal.

  • PmsetSleepGuardPromptTests: disableSleep(_:) delegates to the prompt with its PendingStart unchanged; a cancel surfaces as .cancelled; a .stillRunning surfaces with its handle. The sudo -n paths are not exercised (they would run the real sudo).

  • SleepPromptLifecycleTests: start shows the dialog once, after the journal and the backstop; cancel and failure roll back with session.json gone, journal clean and the "Session not started" notification naming the prompt; a hung dialog leaves session.json, sleepDisabledByUs and an armed backstop in place while it is up, then rolls back on timeout; reconcile with SleepDisabled 1 continues without a prompt; with SleepDisabled 0 ends with the "turned back on" notification and no prompt; an unreadable setting ends without a prompt; extend, Low Power changes, lid undo, countdown pause/resume, end and relaunch never show the dialog, and one Start shows it exactly once. Review fix: a stuck prompt (fake pid 4242) is reported with its pid while session.json, sleepDisabledByUs and the recovery lock (checked with RecoveryLock.tryAcquire) stay, an end requested meanwhile runs nothing until the handle resolves, and the rollback then leaves everything clean with one disablesleep 0. kill 4242 is on the menu line while the prompt runs, never in the notification, and gone from the menu line after the exit. Codex fixes: the marker holds the nonce while the dialog is up and is gone after every outcome; it is gone as soon as stillRunning arrives; when osascript exits while its output is held, the menu line drops the kill at once, with no second notification and no rollback yet. Each start writes a fresh nonce. A directory at pending-start rolls the start back with no dialog. Relaunch path: session.json, the journal entry and an orphan's marker on disk with sleep on, reconcile ends the session and deletes the marker, and the real root command with the orphan's nonce then runs nothing; after a newer start has written its own marker, the orphan's nonce still runs nothing. Any transaction (here a Low Power change) clears a leftover marker.

  • RecoveryScriptTests: install.sh writes exactly the three lines and no disablesleep 1, in one sudo install; a reinstall over a four-line rule leaves three, also in one write. Review fixes: sudo -v is the first sudo call, before the quit. A failed password with the app running stops with no quit and everything untouched; a cancelled password during a session also leaves session.json byte for byte, and the session line is printed before the password step. On a pty (the fixture's terminalInput), "n" at "Continue?" stops with no sudo call and "y" goes ahead; an expired session asks nothing; without a terminal the line is printed and the install goes ahead. A credential that expires during the quit is asked for once more and the install finishes; a second password that fails after the quit leaves everything else untouched and says the app was quit. An app that keeps running stops after sudo -v with no other sudo call and the sudoers file untouched; a rule that is not effective prints the rerun note; an app opened again during the password prompt stops after the rule with three lines on disk, the old bundle in place, and the rerun note; no non-comment line of install.sh mentions disablesleep 1. The lock test now expects one more pgrep check. The two key tests fail against 8d57dad's installer, and the five order tests fail with the up-front sudo -v removed. Fixture scripts now get /dev/null as stdin instead of the test runner's, so a terminal-run swift test cannot block on the new question. Existing uninstall tests still show the file removed. Codex fixes, backstop path: with the app dead under its dialog and the session expired, the backstop deletes the marker before its first sudo call (the fake sudo records whether the marker existed), restores, logs the deletion, and the real root command with the old nonce then runs nothing; with the session still valid it deletes the marker and changes nothing else; with the lock held it leaves the marker byte for byte. Uninstall deletes the marker before the backstop it runs (a stub that records it), and refuses to remove anything, the sudoers rule included, when the marker cannot be deleted.

  • Second review round, new or changed: PendingStartRemovalTests (new; a marker and a missing one, waiting for a lock to be let go, a held lock giving .markerBusy with the file kept, an immutable file and a directory reported, a link to nothing removed). testCancelTextInACommandsOutputIsNotACancel (a pmset failure whose output mentions (-128) mid-text is .failed) and testOnlyCancelAndLaunchFailureRanNothing. Lifecycle: testCancelLeavesASleepSettingSomeoneElseOwns (cancel with a foreign SleepDisabled 1 runs no disablesleep 0), testLaunchFailureRollsBackWithoutPmset, testCancelKeepsAnEntryAnEarlierRestoreLeft (the rollback puts back the journal exactly, including an entry a failed earlier restore left), testRelaunchWhileTheAbandonedDialogsCommandRunsKeepsTheSleepEntry (relaunch while the old dialog's command holds the lock: sleep restored, entry kept, "Restore incomplete", Start refused; after the command is done a reconcile clears it and the menu line), testUndeletableMarkerKeepsTheSleepEntryAndRefusesStarts (chflags uchg), testStuckPromptWhoseCommandHoldsTheMarkerClearsItAfterExit; the cancel test now expects only disablesleep 1 and the new body. Scripts: testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked (exit 1, entry kept, then cleared on the next run), testBackstopKeepsTheSleepEntryWhenTheMarkerCannotBeDeleted (uchg), testBackstopFailsOnAStuckMarkerEvenWithACleanJournal, testUninstallAbortsWhileTheMarkerIsLocked, testScriptsDeleteTheMarkerWithoutPATH (an rm first on PATH that leaves the marker does not stop either script). testTimeoutSendsSigtermOnlyAndWaitsForTheChildToExit sets a 20 s stop grace and accepts .stillRunning (asserting the grace it reports, then waiting on its handle) as well as .timedOut, so a slow TERM handler cannot fail it; with a 1 s grace the new body passes and the old one fails.

  • Second-round mutation checks, one at a time, each failing the named tests: the root command without lockf (script literal, waits-on-removal, cannot-remove-while-pmset, relaunch tests); lockf without -k or without -n; removePendingStart unlinking without the lock (held lock, cannot-remove, relaunch-in-flight, stuck-prompt tests); restoreAll clearing the entry despite a stuck marker and Start not refused (relaunch-in-flight, undeletable); cancel through the undo path (the four cancel and launch tests); no second removal after a stuck prompt exits; (-128) matched anywhere in stderr; backstop.sh and uninstall.sh deleting without lockf; backstop.sh clearing the entry despite a stuck marker, or exiting 0 with a clean journal; a bare rm in either script's lockf command or in the backstop's fallback.

  • Each Codex fix was checked against its tests: with the menu line replacement removed, the stuck-prompt test fails; with the marker deletion removed from exclusive(), the relaunch and any-transaction tests fail; with it removed from backstop.sh, both backstop marker tests fail.

  • Updated: ReconcileTests and ReconcileLidGatingTests now seed SleepDisabled 1 and expect pmset -g instead of disablesleep 1; FakeSleepGuard routes disablesleep 1 through a FakeAdministratorPrompt (succeed, cancel, fail, hang) so every manager test can see whether a path would have prompted.

  • Third review round, new or changed:

    • BackstopVersionTests (new, 5 tests): the repository's backstop.sh declares the required version; only the first version line counts; 2 and above pass; an older line, no line and a missing file all refuse, naming the path and "run scripts/install.sh again"; LaunchdBackstop.checkVoidsPrompts() checks the script its agent runs. testStartWithAnOlderBackstopShowsNoPrompt: no dialog, no pmset, no backstop arm and nothing written, and the next start with a current script shows the dialog.
    • testStuckPromptWhoseMarkerIsGoneIsRolledBackWithoutWaiting uses an osascript fake that never exits. The start returns with the rollback done and the recovery lock free, and end returns .restored. The menu keeps kill 4242 until osascript exits, then names the root command, and the line goes once the prompt exits. A later start shows a new dialog. testVoidedPromptWatcherLeavesALaterLineAlone: a line set later survives the prompt's exit. testStuckPromptWhoseCommandHoldsTheMarkerIsWaitedFor replaces the two earlier stuck-prompt tests and keeps the wait for a command that holds the marker's lock. If a start waits when it should not, the voided tests release the prompt and fail instead of hanging.
    • RootCommandTests: testDoesNothingOnceTheSessionHasEnded (deadlines of one second ago, now and a day ago: exit 4, no pmset) and testAnUnreadableDeadlineNeverPasses (empty, words, 1e12, hex, digits with a suffix, a 20-digit overflow, $(...): exit 4, no pmset). testPasswordTypedAfterTheSessionsEndTurnsNothingOff: a 60 s session answered 90 s later rolls back, with the fake prompt applying the same rule. The start-shows-prompt test checks that the deadline is the session's endsAt. The osascript argument test checks that 1800000900.9 arrives as 1800000900.
    • testSleepRestoreWhoseJournalClearFailsIsReported: an immutable state.json after a successful undo gives .incomplete, the menu line, the "Restore incomplete" notification and the [error] log line, and keeps the entry; the next reconcile clears it.
    • testAnAnswerThatWaitsOnARemovalRunsNothing no longer sleeps 300 ms. waitUntilLockfWaits(under:) waits until lockf is blocked in the kernel on the marker's lock: every thread in TH_STATE_WAITING and no Unix system call across five looks 10 ms apart. It passed 15 of 15 runs.
    • RecoveryScriptTests: testInstallReplacesTheBackstopBeforeTheBundleUnderTheLock (a fake codesign records whether the installed backstop is the new one and whether the recovery lock is held when the bundle is signed), testInstallWaitsForAnOlderBackstopRunBeforeTheBundle, testInstallStopsBeforeTheBundleWhileAnOlderBackstopRunStays, testInstallStopsBeforeTheBundleWhenBackstopRunsCannotBeListed, and testInstallCleansUpWithoutPATH. That last one puts shadows for rm, rmdir, mkdir, cp, install, mv, mktemp and cat first on PATH; the cat shadow adds NOPASSWD: ALL to any rule it is given. It checks that the temporary sudoers file goes after a step 2 stop and after a full install, that no shadow is called for the bundle, backstop.sh or LaunchAgents, that mktemp never comes from PATH, and that the installed rule never gets the added line. The fake sudo knows visudo, install, test and rm only by full path. testUninstallFindsAndRemovesARuleOnlyRootCanSee (new, afc4948) covers uninstall.sh's sudo test -e branch for a rule in a directory the user cannot search; no test reached that branch before. The lock-refusal and started-again tests now also check that the old backstop.sh is untouched.
    • Merge with main: main's new reconcile tests in AppNapTests (App Nap: opt in, journal the previous value and put it back #27) and the stricter testDeadlineTimerFiresEnd (Tests: wait for an end to finish, not start, before checking what it restored #44) assumed reconcile turns sleep off again. Here reconcile reads pmset -g, so those tests seed SleepDisabled 1 and expect pmset -g, as the other reconcile tests in this PR do (f88e93b). Tests: wait for an end to finish, not start, before checking what it restored #44's testEndDuringReconcileMustNotLeaveSleepDisabled parked reconcile's disablesleep 1 on a gate and hung here, because that call no longer exists. It now parks reconcile at its pmset -g read and still checks that the end queues behind it and that disablesleep 0 comes last (b01a97b). An end that skips the queue fails it.
  • Third-round mutation checks, one at a time; each failed the tests named:

    • try? back in restoreAll: the journal-clear test.
    • A bare rm in either trap, or a bare cp, mkdir, install, mv, mktemp or cat: the PATH test.
    • A bare visudo or install handed to sudo: the install tests, because the fake sudo refuses it and the rule step fails. A bare test handed to sudo in uninstall.sh, or no sudo test fallback: the new uninstall test. A bare rm handed to sudo: that test and 12 other uninstall tests.
    • No retire wait, pgrep failure ignored, the path filter loosened to any backstop.sh, the backstop after the bundle, the bundle before the lock: the install-order tests.
    • No version check at Start, >= 1, checkVoidsPrompts as a no-op, the repository script at version 1: BackstopVersionTests and the older-backstop test.
    • The voided branch waiting for the prompt, or disabled; the watcher keeping the kill hint, or clearing any line; the non-voided path not clearing the marker after the exit: the stuck-prompt tests.
    • The root command without the deadline check, with -le for -lt, with the test inverted, or in the if [ now -ge $3 ] form that lets an unreadable deadline pass: RootCommandTests.
    • The start passing endsAt + 1 h, osascript without the deadline argument, rounding up: the lifecycle and argument tests.
    • The handshake removed: the removal test.
    • Not caught: a handshake that checks only lockf's path, not that it is blocked. That only shortens the wait, which is all the 300 ms sleep did.
    • Not mutation-checked: the ProcessExit switch. The hang it removes needs a new Process at the address of an earlier one, which only Process: wait for children with an exit handler, not waitUntilExit #46's ProcessExitTests sets up.
  • Fourth review round, new or changed:

    • Restore check: testStartWithoutThePasswordlessRestoreShowsNoPrompt (no dialog, no pmset, no arm, nothing written, the refusal text, then a start with the rule shows the dialog) and testThePasswordlessRestoreIsCheckedBeforeAnythingIsWritten. PmsetSleepGuardPromptTests runs a fake sudo from a temp dir, never the real one: testRestoreCheckListsTheExactRestoreCommand (argv is -n -l /usr/bin/pmset -a disablesleep 0), testRestoreCheckFailsWithoutTheRule and testRestoreCheckFailsWhenSudoCannotRun.
    • Path identity: PendingStartRemovalTests gains testTheMarkerAStartWroteIsRemoved, testAReplacedMarkerIsNotTakenForTheOneWritten, testAMarkerDeletedWithoutItsLockDoesNotCountAsRemoved, testAFileSwappedInAfterTheOpenIsLockedBeforeItGoes and testAMarkerThatKeepsBeingReplacedIsReported. testAReplacedMarkerIsNotDeletedWhilePmsetRuns uses the real root command and lockf. testStuckPromptWhoseMarkerWasReplacedIsWaitedFor covers the start. For the scripts, testBackstopLeavesAMarkerReplacedAfterItWasLocked and testUninstallLeavesAMarkerReplacedAfterItWasLocked patch in a lockf wrapper that swaps the file right after the lock, and testBackstopDoesNotOpenAFIFOAtTheMarker covers a FIFO at the path.
    • Stuck marker exit: testBackstopFailsOnAStuckMarkerWhileTheSessionIsValid and testBackstopFailsOnAStuckMarkerAfterMovingASessionAside.
    • Installer: the fake pgrep now always lists /usr/bin/tail -f <installed path> and can list a --force run, and the fake visudo records whether the lock is held and backstop.sh is new. New: testInstallDoesNotWaitForAProcessThatOnlyNamesTheBackstop, testInstallWaitsForAnOlderForcedBackstopRun, testInstallStopsBeforeTheRuleWhenTheAppIsOpenedDuringTheWait and testInstallStopsAfterTheRuleWhenTheAppIsOpenedBeforeTheBundle, which replaces the password-prompt test. The lock-held, started-again, retire-timeout and pgrep-failure tests now expect the sudoers file unchanged and no rerun note. The two rule-not-effective tests are renamed to "StopsBeforeTheBundle" and expect the new backstop.sh. The order tests check that the rule comes after the last look and before codesign.
    • TestIsolationTests (Tests: keep every test run inside a temporary INSOMNIA_HOME #36) also checks that pending-start resolves inside the test home.
  • Fourth-round mutation checks, one at a time. Each failed the tests named:

    • No restore check at Start: both lifecycle tests. The check moved after the journal write: the before-anything test. No -l, or the wrong command listed: the restore-check tests (and the no-prompt test for the wrong command). The exit status ignored: the missing-rule test.
    • The app without the path check: the swapped-after-open and keeps-being-replaced tests. Without the written-identity check: the replaced-marker, root-command and stuck-prompt tests. The identity taken after the rename: three tests that expect a voided prompt. A missing marker counted as removed: the deleted-without-lock test. The start not passing what it wrote: the stuck-prompt test.
    • Either script without the identity check: its replaced-after-lock test. Either script without the marker lock: the lock and replaced tests. The backstop opening a FIFO: the FIFO test.
    • Either early exit 0 restored, or the helper exiting 0: the stuck-marker exit tests.
    • The substring match back: the no-wait and both wait tests. No --force form: the forced-run test. No look after the wait, or before the bundle: the reopened-app tests. The sudoers step moved back before the lock: eight install tests.
  • Fifth review round, new or changed:

    • FakeRestoreTools puts a fake sudo and a fake pmset in a temp dir; the real ones never run. The fake sudo records its arguments and answers by policy: the rule, listing only (-l passes and a run needs a password, as with another NOPASSWD entry), a cached credential (passes unless -k), or no rule. The fake pmset prints SleepDisabled 0 or 1, or fails.
    • PmsetSleepGuardPromptTests: testRestoreCheckRunsTheExactRestoreCommandWhileSleepIsOn (one pmset -g, then sudo argv -k -n /usr/bin/pmset -a disablesleep 0; it replaces testRestoreCheckListsTheExactRestoreCommand), testRestoreCheckFailsWhenListingPassesButRunningNeedsAPassword (the fake first lists the command without a password), testRestoreCheckIgnoresACachedCredential, testRestoreCheckRunsNothingWhileSleepIsAlreadyOff, testRestoreCheckRunsTheRestoreTheJournalOwes (no read), testRestoreCheckRunsNothingWhenTheSleepSettingCannotBeRead, testRestoreCheckFailsWithoutTheRule and testRestoreCheckFailsWhenSudoCannotRun.
    • SleepPromptLifecycleTests runs a real PmsetSleepGuard on the same fakes: testStartWithTheRuleRunsTheRestoreBeforeThePrompt, testStartIsRefusedWhenListingPassesButTheRestoreNeedsAPassword and testStartIsRefusedWhenOnlyACachedCredentialWouldRunTheRestore (refused, nothing written, no dialog, no arm), testStartWhileSleepIsAlreadyOffRunsNothing (only pmset -g) and testStartRunsTheRestoreTheJournalOwesBeforeThePrompt. testCancelLeavesASleepSettingSomeoneElseOwns became testStartLeavesASleepSettingSomeoneElseOwns, because a foreign 1 now stops Start before any dialog: the start is refused, then works once the bit reads 0. The before-anything and earlier-restore tests check the sleepOffIsOurs value Start passes.
    • ReconcileTests.testNoSessionButSleepDisabledIsLeftAloneAndReported and RecoverySafetyTests.testLateReconcileMustNotClearNewSessionSleepGuard: a start while the foreign 1 stays is refused and changes nothing, and a start after it reads 0 turns sleep off and clears the menu line.
    • RecoveryScriptTests: the fake sudo handles -k and has a cached-credential mode, and the fake pmset answers -g with 0, 1, no line or a failure. testInstallWritesExactlyThreePasswordlessLinesAndNoneTurnsSleepOff checks that pmset -g comes before the -k -n run and that no -l call happens. New: testInstallStopsWhenOnlyTheCachedCredentialWouldRunTheRestore and testInstallRunsTheCheckOnlyWhileSleepReadsOn (1 and a failed read print "not checked" and run nothing; no line and 0 run the check).
  • Fifth-round mutation checks, one at a time. Each failed the tests named:

    • No -k: the cached-credential, listing-only, missing-rule, journal-owed and exact-command tests, at both levels. -l back in place of the run: the same tests.
    • The pmset -g read ignored: the two already-off tests. No read at all: those two, the unreadable test, the exact-command test and the with-the-rule lifecycle test. An unreadable read taken as 0: the unreadable test. The exit status ignored: the listing-only, cached, missing-rule and journal-owed tests.
    • sleepOffIsOurs always true: the someone-else-owns, already-off, with-the-rule and before-anything tests. Always false: the earlier-restore and journal-owed lifecycle tests.
    • install.sh: no -k, or -l back: the cached-credential and three-lines tests. The read ignored, an unreadable read taken as 0, or a missing line taken as unreadable: the reads-on test.
  • Sixth review round, new or changed. Everything runs on fakes; no real sudo, pmset or osascript runs:

    • RootCommandTests runs the real rootCommand under the real lockf, as the user, with a fake sudo and a fake pmset. Invoked by root (no uid recorded, or 0), the fake sudo runs the command, as the -u user if one is given. Invoked by a user, it applies a policy: rule runs only the exact restore and only for the matching uid, listOnly lists but needs a password to run, cached runs unless -k, noRule refuses, and noRootEntry refuses root itself. New: testTheCheckRunsTheRestoreTheEndRuns, testRefusesWhenOnlyACachedCredentialWouldRunTheRestore, testRefusesWhenTheRestoreIsListedButNeedsAPasswordToRun, testRefusesWithoutTheRule, testRefusesWhenRootCannotRunTheCheckAsTheUser, testTheCheckIsForTheUserItIsGiven, testAnUnusableUidRefusesWithoutRunningSudo and testTheMarkerCannotBeRemovedWhileTheRestoreCheckRuns. Each refusal expects exit 5 and no disablesleep 1. The marker-gone, nonce, deadline and waits-on-removal tests now also expect no sudo call.
    • OsascriptAdministratorPromptTests: the literal and argument tests include item 4 of argv and the uid. New: testARefusedRestoreCheckIsReportedWithTheFix, testOtherStatusesAreNotARefusedRestoreCheck and testOnlyCancelLaunchFailureAndARefusedRestoreLeaveNothingToUndo.
    • PmsetSleepGuardPromptTests: testSleepSettingCheckOnlyReads, testSleepSettingCheckRefusesWhileSleepIsAlreadyOff, testSleepSettingCheckTrustsTheJournal and testSleepSettingCheckRefusesWhenTheSettingCannotBeRead replace the fifth round's fake-sudo restore-check tests.
    • SleepPromptLifecycleTests: testStartRunsNoSudoBeforeTheDialog, testStartWhoseRestoreCheckFailsRollsBackWithNothingToUndo, testARefusedRestoreCheckKeepsAnEntryAnEarlierRestoreLeft, testTheSleepSettingIsReadBeforeAnythingIsWritten, testStartWhileSleepIsAlreadyOffRunsNothing, testStartWithAnUnreadableSleepSettingRunsNothing and testStartWithARestoreTheJournalOwesGoesOn. The fifth round's lifecycle tests on a real PmsetSleepGuard with a fake sudo went with the app's sudo preflight.
    • Installer, from Backstop: run only the copy sealed in the signed bundle #28 and kept: a sudo -k -n -l listing or a visudo that ignores SIGTERM keeps the lock until it exits and never gets SIGKILL (testInstallLeavesASudoersCheckThatIgnoresSigtermHoldingTheLock, testInstallLeavesAVisudoThatIgnoresSigtermHoldingTheLockAndTheRuleUntouched).
  • Sixth-round mutation spot checks, two by hand and no broader run: without -k on the user's sudo, 10 RootCommandTests fail, the cached-credential test among them. Without -u "#$4", 15 fail, the no-rule test among them.

  • Review 11: RootCommandTests.testDoesNothingWhenTheRestoreCheckEndsAtOrAfterTheDeadline puts a fake clock first on the root command's PATH. Two mutation spot checks: with the recheck taken out of both copies, that test fails in all three clock cases; taken out of the AppleScript copy only, it fails along with testTheAppleScriptEmbedsTheRootCommandUnchanged and testScriptIsTheExactLiteral. Afterwards git diff HEAD -- Sources was empty and the build was redone from the restored source.

  • Upgrade fix for 6046657261, all on fakes. No real install, sudo, launchctl or signal to a real process:

    • The upgrade fixture installs a build whose binary only records OLD-APP and whose Info.plist has no InsomniaResumeFrozenVersion, with its agent loaded, an expired session and two frozen entries with startedAtMicros. The new build's Info.plist declares the interface, and its binary records whether the recovery lock is held and then runs the fake responder.
    • testUpgradeOverABuildWithoutResumeFrozenResumesWithTheStagedBinary (source) and testUpgradeFromAPrebuiltBundleOverABuildWithoutResumeFrozenResumesWithTheStagedBinary (--app): the install exits 0 and the old binary never runs. The staged copy's binary runs once, from the staging folder, with the lock held and the lock file on its fd 9, before the old agent is booted out. Both entries are cleared and logged as resumed by the app binary, the new binary is installed with one bootstrap, and no staging folder is left.
    • testUpgradeKeepsWhatTheStagedBinaryCannotResumeAndThePreviousPair, five cases on fresh fixtures: a failed SIGCONT, an unverifiable process, a malformed answer, no answer in time, and a staged build that does not declare the interface (no binary runs). Each exits 1 with the stop message and the rerun line. The entries the binary did not resume stay with their startedAtMicros. The old binary, its Info.plist and the agent plist stay byte for byte, nothing is booted out or bootstrapped, the staging folder is gone and the lock is free.
    • testOwnBundleUsesTheBinaryAndInfoPlistBesideItsCopy: a declaring bundle's own binary resumes the entry, though the installed app here would answer too. A bundle beside it whose Info.plist does not declare the interface runs no binary and keeps the entry with its microseconds.
    • testOwnBundleRefusesACopyOutsideABundlesResources: the checkout's copy, a loose copy, a copy in Contents/ instead of Contents/Resources/, and a bundle's copy run by a relative path all exit 2, with no binary run, the journal unchanged, no lock file and an empty log. The same bundle copy run by its full path then resumes the entry.
    • Changed: testInstallKeepsTheTrustedAgentWhenRecoveryIsUnresolved and testInstallQuotesTheCheckoutPathsInTheCommandsItPrints expect the installer's rerun command and no backstop.sh --force line.
  • Round 16, new or changed. No real sudo, pmset or osascript runs:

    • RootCommandTests runs the command read back from the AppleScript literal and rootCommand itself. The fake pmset records who ran each call, root or the user's uid, and can set SleepDisabled 1 after any chosen call, as another tool would. A run that passes now makes five calls: -g, -a disablesleep 1 as root, -a disablesleep 0 as the user, -g and -a disablesleep 1. A refused proof makes three and leaves 0.
    • testEveryPolicyButTheRuleLeavesSleepOn: a cached credential, a listing-only policy, no rule, a rule that needs a password (PASSWD), an explicit deny and a sudoers with no root entry each exit 5 with SleepDisabled 0 and root's restore as the last call, in both literals. Only the three-line rule exits 0.
    • testWhereAnotherToolsOneLandsDecidesWhetherItSurvives: eleven rows put one foreign 1 at one point, through a passing check, a failing check and a deadline that passes during the check. Set while the dialog was up, or after the proof, the 1 survives every path. Set between the first read and root's change, or while that change is in effect, it does not. These rows record the limit; they do not remove it.
    • Also new: a 1 set while the dialog was up with the deadline passing during the check, a 1 set after the check, an unreadable setting before and after the check, a journal-owned 1 (both reads skipped, and root restores 0 when the check fails), $5 other than exactly "1" read as not owned, the read parsed as Start parses it, root's restore failing (exit 1, not a refusal) and the dialog's output closed before the command runs (testAClosedDialogCannotStopRootsRestore: root's restore still runs).
    • OsascriptAdministratorPromptTests: the fifth argument, testRootRefusalsLeaveNothingToUndo (3, 4, 6, 69 and 75) and testOtherRootStatusesAreFailuresToUndo.
    • StartOwnershipEndToEndTests (new) runs a real SessionManager, PmsetSleepGuard and OsascriptAdministratorPrompt. Its fake osascript runs the real root command under the real lockf against one fake machine. A 1 set while the password is typed survives the start and a late end. A late password runs nothing. The dialog is told whether the journal owns the 1. Start then end turns sleep off and on. A 1 set after the check survives a late end. An end during the check rolls back with no undo, and so does a failed check. Two cases record the limits: an ambiguous failure still undoes a 1 set while the dialog was up, and a 1 set right after the command's read is still cleared.
    • SleepPromptLifecycleTests checks the new texts.
  • Round 16 mutation checks. Each mutant was applied to both literals, or to the Swift mapping for the last one, and run against OsascriptAdministratorPromptTests, RootCommandTests, SleepPromptLifecycleTests and StartOwnershipEndToEndTests (98 tests). Every mutant failed tests. The source was put back after each, its SHA-256 matched the original, and the tests were rebuilt.

    • No root disablesleep 1 before the proof: 44 tests, seven of them end-to-end.
    • No root restore after a failed proof: 13 tests, among them the policy test and the eleven-row table.
    • The message written before root's restore: 2 tests, the closed-output test and the failing-restore test.
    • No deadline check after the proof: 5 tests. No second read: 15. No first read: 34. No -k on the proof: 18.
    • .restoreNeedsPassword undone like an end: 5 tests.
  • Round 18, new or changed. No real sudo, pmset or osascript runs:

    • The fake sudo answers the three queries with text built from sudo's source (1.9.15, 1.9.16, 1.9.17p2 and Apple's sudo-114.100.11), by policy: the rule under either file name, another NOPASSWD entry only (listOnly), no NOPASSWD entry, the rule without NOPASSWD, an explicit deny, a later rule without NOPASSWD, Defaults!/usr/bin/pmset log_output, (ALL), an extra tag, a NOTAFTER limit, a rule from LDAP, a path-only answer, a truncated answer, sudo 1.9.14p3, an approval plugin and no root entry. It records every call and refuses anything the root command never asks, a query without -k -n included. A fake env checks env -i LC_ALL=C, and a fake clock can move after any chosen call.
    • RootCommandTests: testTurnsSleepOffOnceSudoConfirmsThePasswordlessRestore (three queries, one read, one write), testTheQueriesNameTheRestoreTheEndRunsAndRunNothing (exact argv), testEveryOtherPolicyRefusesBeforeAnyPmset (every policy above exits 5 at the first answer that does not fit, with no pmset call, in both literals), testTakesTheRuleUnderEitherNameOfItsFile, testTheQueriesAreForTheUserTheyAreGiven, testWritesNothingWhenTheDeadlineComesDuringAnyCallBeforeTheWrite (R3: at, 1 s and a day past the deadline, during each query and the read), testAJournalOwnedSettingWritesNothingWhenTheDeadlineComesDuringTheQuestions, testARefusalKeepsItsStatusWhenTheDialogsOutputIsGone (R2: exits 3, 4, 5 and 6 with stderr closed, none writing), testAFailedWriteIsNotARefusal, testLeavesASleepSettingMadeWhileSudoIsAsked and testASudoRefusalLeavesASettingMadeWhileSudoIsAsked (R1 and R2), testTheMarkerCannotBeRemovedWhileSudoIsAsked, and the eleven-row testWhereAnotherToolsOneLandsDecidesWhetherItSurvives, where a 1 set before root's read now survives every path and only one set between the read and the write is taken for Insomnia's.
    • RootCommandSudoAnswerTests (new) runs each awk reader alone. The version reader takes 1.9.15 to 1.9.x with the sudoers plugins in order, and refuses 1.9.14p3, 1.10 and 2.0, other plugins, root's long answer, a missing grammar line, a repeated or misordered plugin line, a trailing blank, CRLF and nothing. The listing reader refuses the bound-Defaults section. The rule reader takes only the six lines, under either file name, and refuses PASSWD, another option, another run-as user or group, pmset with any arguments, two commands, another matched command, another file, a Timeout line, spaces for the tab, CRLF, a second entry, an extra or missing Matched line and nothing.
    • StartOwnershipEndToEndTests rewritten for the new order: start and end, a 1 set while the dialog is up or while sudo is asked is left alone, a late password and an end during the queries or the read write nothing, a refused query leaves sleep on with no undo (no rule, a later rule, an old sudo), and a 1 set right after the read is cleared only when the write follows.
    • Replaced assertions: tests that expected the temporary 1, the user's run of the restore or root's fallback (round 16's five-call order, "not left off", testAClosedDialogCannotStopRootsRestore) now expect no write before the queries pass. Those expectations described the unsafe order R1 removes. Lock, settlement, deadline and upgrade assertions are unchanged; testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked and the relaunch test hold the command at its write and expect its two pmset calls (-g, -a disablesleep 1); their sleep-entry and marker-lock assertions are as before.
  • Round 20, new or changed. No real sudo, pmset or osascript runs:

    • The fake sudo reads the same fixture sudo.conf the command checks. With a Plugin ..._approval line, an approval plugin with no show_version, its -V, -l and -ll answers stay those of the rule and only running the restore fails, as sudo.c does (testTheSilentApprovalFixtureChangesNothingButTheRestore). A new userDefaults policy lists the review's log_output, !ignore_iolog_errors and iolog_dir and fails the restore.
    • RootCommandTests: testAnySudoConfStopsTheCommandBeforeSudoRuns (the plugin, an empty file, comments only and a setting with no plugin, in both literals: exit 5 with no sudo and no pmset call), testOnlyMacOSsOwnPamSessionLinePasses, testTheMarkerTakesTheRecordOnlyAfterEveryCheck, testTheRecordIsWrittenInPlaceAsTheUser and testAMarkerThatCannotTakeTheRecordStopsBeforeTheRead (7).
    • RootCommandSudoAnswerTests: the version reader takes only 1.9.17p2 with grammar 50 and the sudoers plugins. The listing reader takes macOS's own Defaults and each listed entry, and refuses 29 other answers: the review's Defaults, log_output, !ignore_iolog_errors, iolog_dir, logfile, use_pty, requiretty, rootpw, !authenticate, group_source, preserve_groups, runas_default, bound Defaults, a list operator on a flag, a backslash, an escaped comma, a tab, an unknown or upper-case name and layouts it cannot read.
    • PendingStartRemovalTests.testOnlyTheFileTheStartWroteStillHoldingItsNonceIsUntouched.
    • SleepPromptLifecycleTests: a failure before the record leaves a 1 set meanwhile, and one after it is undone; a replaced marker holding the nonce, and no marker, are undone; a stuck prompt whose command exits before its record is rolled back with no undo, and one whose marker held the record is undone.
    • StartOwnershipEndToEndTests: a command stopped by SIGTERM while sudo is asked (lockf 70) leaves another tool's 1; one stopped at root's read, after its record, is undone and clears it (the limit); a sudo.conf and the user Defaults stop the start before any pmset.
    • Replaced: testAnAmbiguousFailureStillUndoesASettingMadeMeanwhile is now testAnAmbiguousFailureAfterTheRecordStillUndoesASettingMadeMeanwhile, and the 1.9.15-and-later and bound-Defaults-only reader tests are now the 1.9.17p2 and Defaults-list tests. The test interrupt hook is pkill -TERM -a -P, because macOS pkill leaves out its own ancestors without -a.
  • Round 22, new or changed. No real sudo, pmset, osascript, install.sh or uninstall.sh runs, and no test writes under /private/var/db:

    • The tests' receipt is a file in their temporary folder, made as install.sh makes it. SleepOffReceipts built with the test user's uid trusts it; the root command and the scripts trust it only in private copies whose -v o=0, RECEIPTS and RECEIPT_OWNER lines are patched. The fake sudo maps install.sh's install, mkdir, rm and rmdir of the receipt's fixed paths to that folder. Unsafe states are made with chmod, a second link, a symlink, a replacing rename and an allow ACL entry; publication failures with an immutable flag (chflags uchg) and read-only folders.
    • SleepOffReceiptsTests (new): the verdict follows the nonce and the word (testTheVerdictFollowsTheNonceAndTheWord); a receipt that is another file now, with the same bytes, shows nothing (testAReceiptThatIsAnotherFileNowShowsNothing); so does every receipt or folder the checks do not trust: missing, not 45 bytes, a malformed line, a second link, a symlink, group or other write, an allow ACL entry, a folder that is a link or writable by others (testAReceiptOrFolderTheChecksDoNotTrustShowsNothing); SleepOffReceipts.live trusts uid 0 alone, and a receipt the user owns is refused (testOnlyRootIsTrustedAndAReceiptTheUserOwnsIsNot); only a plain absolute folder is accepted; identities are what stat prints.
    • SleepOffSettlementTests (new), each through the real SessionManager with fake commands: a relaunch with an unfinished start does not resume its unexpired session on another tool's 1, and does not restore an expired one that never turned sleep off; it keeps the restore an earlier session owes; it undoes a start whose receipt shows writing, and one whose evidence does not match (the receipt replaced, the marker replaced or already gone, the receipt missing); it settles a start that showed no dialog as never turning sleep off; it does not resume a session beside a marker no journaled start accounts for, with the plain session as its control. A settlement that cannot be written ends the session and refuses Start until it can (a read-only home, then an immutable state.json, each released and settled afterwards). An earlier start's writing does not count for a new start; a replayed dialog writes nothing; Start refuses a receipt the user owns under the shipped trust; a normal start and end leave no record. The matched pair: testAFailureAfterTheWriteIsUndone and testAForgedMarkerDoesNotDropTheRestore.
    • RootCommandTests: testTheReceiptTakesTheRecordOnlyAfterEveryCheckAndTheRead (install.sh's content while sudo is asked and while root reads, this nonce with writing from the write on, the marker unchanged), testTheReceiptIsWrittenInPlaceAndTheMarkerNeverIs (same inode, 45 bytes, no write as the user), testAReceiptThatCannotBeWrittenStopsBeforeTheWrite (exit 7 after the read, no pmset write), testAMissingOrUnsafeReceiptStopsBeforeTheRead (exit 7, nothing read or written), testTheShippedCommandTrustsOnlyRootsReceipt and testOnlyAnUppercaseUUIDNonceReachesTheReceipt. Every other test reads the command's record from the receipt where it read the marker.
    • PendingStartRemovalTests.testTheFileThatGoesIsReportedByIdentityAlone replaces testOnlyTheFileTheStartWroteStillHoldingItsNonceIsUntouched: the removal reports which file went, and no longer what it held.
    • RecoveryScriptTests (backstop.sh version 3): it settles a start whose receipt shows no write, or this start's refused, keeps an earlier owed restore, leaves a session that is not the start's, undoes a start whose receipt shows writing, is another file, or cannot be trusted, undoes a start whose marker is not the one it wrote, settles a start that showed no dialog, waits while the command behind the dialog holds the marker, and restores sleep, keeps the start and exits 1 when the settlement or the session's removal cannot be published. uninstall.sh settles before its backstop, removes the receipt and the folder through sudo, stops with "Nothing was removed; rerun." when the settlement cannot be published, leaves a folder someone else could change, and keeps the folder while another account's receipt is in it. install.sh creates the receipt through sudo by its fixed paths, keeps one it made before, and stops at a receipt or folder it did not make without changing its owner or mode.
    • Replaced: testBackstopVoidsAnAbandonedDialogEvenWhileTheSessionIsValid asserted that the valid session stays beside a marker no journaled start accounts for. It is now testBackstopEndsAValidSessionBesideAMarkerNoJournaledStartAccountsFor (the session goes and the entry is undone), with testBackstopLeavesAValidSessionWithNoMarker as the control that keeps the old outcome. testACommandStoppedBeforeItsRecordLeavesASettingMadeWhileTheDialogWasUp gains the read as a stop point, and testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite now stops the command at its write, since the record follows the read; it still asserts the undo and the cleared 1. Notices that said the command "never reached the sleep setting" now say the receipt shows it never turned sleep off. RootCommandSudoAnswerTests counts 6 awk programs (the receipt check added one). BackstopVersionTests requires version 3 and refuses 2. Two install tests that counted every sudo -n /usr/bin/install call now require exactly one for the sudoers file and the receipt's as the only other.

Decisions

  • osascript child, not NSAppleScript in-process. AppleScript is main-thread only. A dialog waited on from the main actor would freeze the menu bar, the lifecycle queue (which holds the recovery lock) and every timer for as long as the dialog is up, with no way to time out, and a late answer after a rollback would turn sleep off with a clean journal. A child can be waited on from a background queue and signalled at the deadline. The cost is that the dialog's app name may read as osascript; the with prompt text states what Insomnia is doing, and the hardware row asks the maintainer to check how it reads.

  • SIGTERM only, a bounded wait, and the lock stays with the command. The command osascript runs after authentication is a root pmset that a SIGKILL could not reach, so nothing is ever killed. The runner answers the caller 3 s after the deadline instead of blocking on pipe EOF, carrying the pid and a handle. The manager does not release the lock on that answer: the command may still turn sleep off, and a backstop or reconcile that ran beside it would clear a journal the late pmset then contradicts. So the transaction keeps the lock and session.json (third round: unless it deleted the marker under the marker's lock first, below), tells the user what is running (a pid they can kill only while it is osascript's own; once osascript has exited the pid may be reused and the holder is a root command), and rolls back after the exit. Other transactions queue behind it rather than being refused with a reason; Sleep guard: never SIGKILL sudo pmset; keep the lock while it runs #22 adds that refusal path and the lock hand-off for sudo pmset, and the maintainer can route stillRunning onto CommandStillRunningError once both are in. This PR does not touch CancellableCommand.

  • 120 s limit. The user is typing a password; shorter limits would cut off a slow reader, and the journal and armed backstop already protect the interval.

  • Relaunch never prompts. With SleepDisabled 0 the session ends and says why, rather than prompting with nobody at the keyboard. A failed pmset -g read also ends it (the existing recoveryUnavailable reason, whose notification now covers "or the sleep setting").

  • No other start path exists. The only caller of start(duration:) is the Enter key in the menu bar. There is no auto-start, URL scheme, scheduled start or command-line start, and launch at login only reconciles. If one is added later it must not reach setSleepDisabled(true); the protocol comment and testOnlyStartShowsThePrompt say so.

  • Failed installs leave the rule in place, as before. With this change the leftover lines can only turn sleep back on or toggle Low Power Mode on battery, and an older LaunchAgent still loaded after a failed install needs them to recover. Removing the file would also need a second sudo prompt. SECURITY.md says the rule stays after a failed install.

  • Password, then quit, then the rule, then the bundle. The password-first step is the maintainer's design after the third review: with the quit first, a cancelled password ended a running session and upgraded nothing. sudo -v asks before the app is touched; the rule is then written on the cached credential, with one more prompt only if the up-to-15 s quit outlasted it. The session line and the "Continue?" question come before the password, follow Install: build the sudoers rule from the account, show it, and confirm #20's [[ -t 0 ]] and read -r -p style, and are skipped without a terminal so scripted installs still run. Earlier history: quit first, then the rule, then the bundle, as the maintainer suggested after the second review. The first review fix kept the rule first and put the old disablesleep 1 line back when the app refused to quit; Greptile flagged that as re-granting the exposure this PR removes, and it did. Now the installer never writes that line. A refused quit changes nothing. A failed second password after a successful quit leaves the old bundle and its rule as they were, so the old build can be opened again; the message says the app was quit. A stop between the rule and the new bundle fails closed: an older build cannot start a session until the rerun, and the note says so with the command. The app is checked again after the password prompt, because the prompt is now between the quit and the bundle and the app may have been reopened. Fourth round: the rule now comes after backstop.sh and the retire wait, still under the lock (below).

  • FakeSleepGuard keeps its call strings (disablesleep 1, disablesleep 0), so the other workers' tests that read calls keep applying; its methods follow the protocol split.

  • A pending-start marker with a nonce, checked by the root command, voids a dialog that outlived its start (Codex P0). The app cannot close the dialog when it dies: osascript is reparented and keeps running, and a SIGKILL would not reach the root command. The root command cannot read the journal without a JSON parser. A file that only lock holders delete ties the dialog to the recovery lock instead: the start holds the lock for the whole dialog, so whoever else holds it knows no start is waiting and may void any dialog still on screen. A nonce rather than mere presence means an old dialog cannot act on a newer start's marker. The marker path and nonce are positional parameters of fixed command text, so nothing reaches the root shell as code. The sudoers rule keeps its three lines.

  • A lock on the marker instead of a second read and compensation (second review round, a deviation from the suggested re-check). Both reviewers showed the second read had its own failure modes: its disablesleep 0 could fail with nobody told, a failed disablesleep 1 skipped it, and an old command that found a newer start's nonce turned sleep back on in that session. Checking that compensation worked would still leave a window between pmset and the check. With lockf -k -n around the check and pmset, and every deleter taking the same lock before it unlinks, there is no window: the marker goes before the check (nothing runs) or after pmset (the journal still covers it, and the same recovery restores sleep). An old command cannot reach a newer start's marker either, because a start writes its marker only after its own transaction removed the old one under that lock. lockf is a fixed path with fixed flags, the marker path reaches it through quoted form of, nothing read from a file is evaluated, and the sudoers rule keeps its three lines.

  • An unremovable marker is reported, not worked around. The app and the backstop restore sleep anyway, because a Mac that cannot sleep is the worse outcome, but keep sleepDisabledByUs while the file is there so a late answer to the old dialog still has a journal entry behind it. New starts are refused while it is there, because a start may write its marker only after it removed the previous one under that lock, which is what keeps an old command away from a newer nonce; every transaction and every backstop run retries, so the state clears on its own once the command exits or the flag is removed.

  • Definitive and ambiguous prompt failures. Only a cancel and a launch failure prove nothing ran as root, so only they get the exact rollback without pmset. A cancel is recognised only when osascript's trimmed stderr ends in (-128), the form do shell script gives a cancelled dialog; a command whose output merely contains that text is a failure. A wrong password, a timeout and a pmset failure keep the undo path, because pmset may have run.

  • 10 s for the marker lock, matching the recovery lock. pmset returns in well under a second; a longer hold means a stuck root command, which is reported rather than waited on.

  • Splitting setSleepDisabled(Bool) into disableSleep(PendingStart) and enableSleep() makes a marker part of the call's type, so a later caller cannot reach the dialog without one.

  • A voided prompt does not hold the recovery lock (third round, maintainer direction). The lock rule is that the lock stays while a command that may still change something runs. Once clearPendingStart() has removed the marker under the marker's own lock, the root command cannot reach pmset. If it has not started, lockf -n exits 69. If it was waiting on that lock, it gets the lock on the unlinked file, reads nothing at the path and exits 3. Keeping the lock and session.json for a process that can change nothing would only block ends and the agent, so only the menu line keeps following it. If the removal fails because the command holds the lock, it may be in pmset, and the old wait applies. The rule text in .greptile/config.json says this.

  • The deadline is $3, not marker content. Both come from the app running as the user, so neither is more trusted. $3 keeps the marker a bare nonce (its format, its two script readers and the head -c 64 read stay as they were) and adds one if to the root command, so the change stays small and the maintainer's fallback (the app voiding the marker at the deadline) was not needed. [ "$(/bin/date +%s)" -lt "$3" ] fails closed: a $3 it cannot compare is an error, and the ! turns that into a refusal. Rounding down makes a refusal come at most a second early, never late. The app still ends a session at its deadline as before; this check only covers a password typed after it.

  • A version line read before every Start, plus install order (third round). The app cannot tell an old backstop from its behaviour, so the script states the contract it implements in a comment line bash ignores. Reading it costs one file read before a dialog the user waits on anyway. install.sh replaces backstop.sh under the recovery lock before the bundle, so the only way to end up with the new app and an old script is an install that stopped before the script, and that app refuses Start. The wait for old runs closes the window in which a run that started before the swap still executes the old code from its own inode. launchd starts one run at a time, and an old run holds the recovery lock for at most its 10 s timeout plus its undo, so 30 s is generous; a run still there after that is reported, not killed. With Backstop: run only the copy sealed in the signed bundle #28, which seals backstop.sh inside the bundle: checkVoidsPrompts() reads LaunchdBackstop.scriptPath, which Backstop: run only the copy sealed in the signed bundle #28 keeps (computed from the bundle), so the check follows the agent's script without changes. Backstop: run only the copy sealed in the signed bundle #28's install.sh already replaces the bundle under the recovery lock, so this PR's backstop step becomes part of that bundle copy when the two are merged, and the version line travels with the script. The retire wait still applies to runs of the old copy under Application Support.

  • The handshake reads kernel state instead of adding a hook to the root command. lockf blocks in the open(2) with O_EXLOCK after it has resolved the path, and its descriptor is not visible while it waits, so the test cannot look for an open file. It finds the lockf child by proc_pidpath and waits until every thread is waiting and its Unix system call count stops moving. The command text stays exactly what runs as root.

  • The restore check runs the restore (fifth round, as the maintainer directed). Only running a command shows whether sudo would ask for a password. sudo -l says whether a command is allowed, and it lists without a password whenever any NOPASSWD entry exists; a cached credential passes both. -k takes the cache out, so exit 0 comes from the sudoers policy alone. The run changes something only while SleepDisabled is 1. A 1 the journal owns is what the next end or backstop.sh run restores anyway, so the check runs it. A 1 nobody journaled belongs to someone else, and the design keeps it: a cancel runs no pmset, and reconcile step 3 only reports it. So Start reads pmset -g first and refuses with the command that clears it, instead of guessing. No code parses sudo -l output. Its format cannot be checked here without the real sudo, and a wrong guess would refuse every Start or pass a rule that needs a password. Round 18 reverses this inside the root command, with the format taken from sudo's source and a refusal on anything else (below).

  • Path identity rather than a new lock (fourth round). lockf locks a file and rm goes by path, so the deleters now prove the path still names the locked file. The scripts lock through fd 8, because lockf <path> closes its descriptor when it exits and leaves nothing to compare. They read the locked file's identity with stat <&8, because stat /dev/fd/8 reports the fdesc device instead of the file's. The maintainer asked for an equivalent in the scripts that keeps the fixed-path rule: this uses only $LOCKF, $STAT and $RM. The one thing the scripts cannot check is in Not covered.

  • Rule after the retire wait, not a put-back (fourth round). A timeout used to leave the new rule beside the old app. Putting the old rule back on a timeout was ruled out, because an older four-line rule includes the passwordless disablesleep 1 line. So the rule moved after the wait: every stop before it leaves the old rule and the old app together, and the only new file is backstop.sh, which the old app does not depend on. The wait matches exact arguments instead of waiting on the lock, because the installer itself holds the recovery lock and the runs it waits for are blocked on it.

  • The restore proof runs inside the root command (sixth round). Greptile's race needs a run of the restore at a point nothing has journaled. Behind the password, the start has already journaled sleepDisabledByUs and armed the backstop, so the run can only make the change the session's end or the rollback would make anyway. It runs as the user, through root's sudo -n -u "#uid", because sudo never asks root for a password, so a run as root would prove nothing. do shell script ... with administrator privileges runs with real and effective uid 0 (Apple TN2065), and macOS's default root ALL = (ALL) ALL lets root's sudo switch users; a sudoers without that entry fails closed. The cost is that the user types the password before a missing rule is reported. The other routes were a privileged helper (ruled out by the maintainer), parsing sudo -l (not proof) and a read-then-run in the app (the race).

  • The installer's recovery uses the staged binary (6046657261). The pre-swap recovery has to settle microsecond entries before the old bundle goes, and the old bundle may not have --resume-frozen. Running it would open the menu bar app, so its entries could only stay and stop the install. The staged copy's binary is the build its backstop.sh was sealed with, codesign has checked both, and it speaks the interface the script expects. A flag keeps the LaunchAgent and uninstall.sh on the installed app. The path comes from BASH_SOURCE[0] rather than an argument or the environment, so no caller can point the script at another binary. The check is of the path's shape only, and does not verify the bundle; the script's header says the caller must run a copy it has verified, and install.sh does. The other route was to swap first and let the new build's agent resume the processes, but install.sh keeps the previous app and agent whenever the recovery leaves entries it could not settle.

  • The proof undoes the command's own change (round 16, within Option 2). pmset has one SleepDisabled bit with no owner and no compare-and-set, so the only change a root command can tell apart is one it made itself. The fresh read as root is what stops the review's reproduction, because it comes after the dialog. Moving the proof after root's own disablesleep 1 makes the proof's write the undo of a change the command just made, which the start had journaled before the dialog, instead of a write over whatever it found before Insomnia changed anything. It also makes every exit the app treats as "nothing to undo" true by construction. It does not make the remaining window shorter than a read followed by the old proof would. The cost is that sleep is off while the proof runs, even when the rule is missing. Not taken: a listing (sudo -l) instead of a run, which the fifth round showed is not proof, and a process-owned sleep assertion or compare-and-set write, which needs the privileged helper Option 2 rules out. Replaced in round 18 (next).

  • The root command asks sudo instead of running the restore (round 18, within Option 2). The round 17 review showed that any write before the permission is known can strand sleep off, and that the proof's write is what overwrote another tool's 1. Given a command, sudo -ll prints the rule that decides it (the 1.9.17p2 manual; display_cmnd is the same in the 1.9.15, 1.9.16 and Apple sudo-114.100.11 sources), so the permission can be read without changing anything. -k keeps a cached credential out and -n refuses instead of prompting, as before. The readers accept one exact shape, the one install.sh's rule gives in those sources, and refuse everything else, so an unknown sudo costs a refused Start, never a write. 1.9.15 is the oldest source read, so older versions refuse. -V is checked first because a listing does not consult approval plugins, and -l because Defaults bound to pmset change how the restore runs without showing in -ll. The costs: a Mac whose sudo or sudoers differs in those ways refuses every Start, a listing is not a run (Not covered), and the format comes from source, not from the installed sudo. Not taken: a generic sudo -l, sudo -v or a NOPASSWD grep (not proof), and running the restore before or after a write (R1 and R2). Narrowed in round 20 (next).

  • Refuse what the answers cannot show (round 20, within Option 2). F1 and F2 are setups in which sudo answers a listing one way and runs the restore another. sudo has no query that shows a silent approval plugin, and a listing runs neither PAM's session stack nor the logging the restore would. So the command takes only the setup whose run it can predict from source: no sudo.conf (macOS installs none), macOS's own PAM session line, the one sudo version read, and Defaults that change no outcome of a run that the listing passed. The cost lands on users who changed those files, and on the next macOS sudo, which refuses until a release is checked against it. Not taken: parsing sudo.conf or naming known plugins (a plugin's name says nothing about what it checks), and a denylist of Defaults (a missed setting would pass).

  • A record in the marker, not a new file or mechanism (round 20). Ambiguous failures were undone because a write may have happened. The marker is already the one file the root command and the app share under one lock, so the command marks it right before it reads the setting, and the app reads it under that lock before it deletes it. The record is written as the user, in place, so the file the app compares is the one it wrote, and root writes nothing into the user's folder. Only the live app reads it: relaunch, backstop.sh and uninstall.sh have no written identity to compare (fourth round), and a bare nonce in a swapped-in file must never cancel an owed restore there. Replaced in round 22, after the round 21 review forged the record from a process running as the user (below).

  • A root-owned receipt plus a journaled attempt (round 22, the review's B and C together, within Option 2). Anything in the marker or the journal can be written by a process running as the user, which is Finding 1. A file only root can write, in folders only root can change, is the one record such a process cannot forge, and the root command already runs as root behind the password, so the receipt needs no new sudoers line, no helper and no new privileged command. The attempt in the journal tells every reader which start, which receipt file and which marker file to compare, and what the journal owed before the start. The costs: one more install step, Start refused until install.sh has made the receipt, and a root write and fsync per Start. Not taken: a receipt in the journal only (the same forgery as Finding 1), dropping the no-undo rollback (that brings back the before-record loss of another tool's 1 the round 19 review found), and a helper making the same writes (no ownership of the bit either).

  • One fixed path per user, rewritten in place. The path is fixed text in the command, the uid must be plain digits, and every folder up to / must be root's alone, so root's write never follows a path a user can change, and root never creates or changes files in the user's folders. dd conv=notrunc,fsync keeps the inode, owner, mode and size install.sh set, so the identity a start journals still matches after the write, and nothing accumulates: one 45-byte file per account. Each start compares its own nonce, so a record an earlier start left can never stand for a new one. No reader running as the user deletes or resets the receipt; only uninstall.sh removes it, through sudo. A rename-based replace was not taken: root would create files in the folder, and the identity would change on every Start.

  • The read before the record. Round 20 wrote the record before root's pmset -g read. Now the read comes first, so a 1 found at the read exits 6 with the receipt unchanged, and writing means only that the read found no foreign 1 (or the journal owned it) and that only a clock comparison and pmset's write remain. A command stopped at its read now leaves another tool's 1, as every refusal does.

  • Settlement removes the unfinished start's session.json whatever the receipt shows. That session never began, because its start never finished, so resuming it would read another tool's 1 as Insomnia's (Finding 2). Only the sleep entry depends on the verdict. A session.json that does not end at the attempt's deadline belongs to something else and is left to the usual reconcile.

  • A failure to record the settlement errs toward the restore. The attempt stays, sleep is turned back on, Start is refused and every run tries again, then with no marker of its own to match, so the second try reads "may have". This can clear another tool's 1 even when the receipt showed no write (Not covered), and it never leaves an owed restore unrecorded.

  • A marker with no journaled attempt drops session.json. Such a marker comes from a build before this round, or from a start that finished or rolled back but could not delete it. Nothing shows whether its command turned sleep off, so its session is never resumed on a 1 that may not be Insomnia's. The app does this only with no session in memory. The cost: a session whose own marker could not be deleted ends at the next relaunch or backstop run.

Merged main

Merged origin/main (5330c28) with a merge commit. Two documentation conflicts, both resolved by keeping both sides: SECURITY.md keeps the three-line sudoers text beside #16's unified-log and Location Services sentences, and spec section 9 keeps the password-prompt notifications beside #29's "battery unreadable twice in a row". Swift files merged without conflicts; #29's new batteryUnreadable end reason and this PR's sleepReenabled coexist, and Start is still the only caller of setSleepDisabled(true).

Merged origin/main again (b5f6de9) after #26 landed. Two conflicts. In SessionManager.swift, reconcile step 3 takes #26's version: it no longer clears a bit Insomnia did not journal, so this PR's enableSleep() call there is gone, and promptStuckTitle sits beside #26's foreign-sleep constants. In spec.md, section 8 keeps this PR's step 2 and #26's step 3, and section 9 lists both PRs' notifications. #26's new tests merged without changes.

origin/main had not moved (afe8c3a), so b180529 needed no merge.

Merged origin/main (178dde8) after GitHub reported a conflict: #24, #27, #35, #42, #44 and #45 had landed. Eight files conflicted, all resolved by keeping both sides. backstop.sh and uninstall.sh keep this PR's RM beside #27's DEFAULTS, and uninstall.sh keeps PENDING beside #27's CONFIG and agent list. FakeSleepGuard keeps this PR's disableSleep(_:) / enableSleep() split with #44's restoreGate and restoreCalledAt folded into enableSleep(). The fixture's run keeps the pty input beside #27's private TMPDIR. .greptile/config.json takes #27's journaled App Nap wording (the old exception is gone) and its builtin-kill sentence, and adds DEFAULTS to the variable list. SECURITY.md and docs/release-validation.md keep both sides' text and rows. SessionManager.swift merged without conflicts: #27's App Nap apply and restore sit beside this PR's start and restore changes, and its failed journal clear is reported the same way as the sleep one here.

Merged origin/main again (4ea445b) for #46. GitHub showed no conflict, but #46 changed the fixture's holdLock() to return a LockHolder, so a test this branch adds no longer compiled on top of main, and CI builds the PR's merge ref. #46 also replaced every waitUntilExit() with ProcessExit; the osascript runner and the test helpers this branch adds now use it too (887bc71). No file conflicted.

Merged origin/main (0c2e791) in 4c7bdfa after #36, #23, #17 and #21 landed. Five conflicts, each resolved by keeping both sides: the notification titles in SessionManager.swift (promptStuckTitle beside main's sessionFileTitle), the StoreError cases (this PR's marker cases beside main's .unreadable and .notRegularFile), the test helpers in TestSupport.swift (#23's FIFOWatch after LockHolderBox), uninstall.sh's tool block (one RM line), and spec section 8 steps 1 and 2 (main's unreadable session.json text, then this PR's step 2). backstop.sh had RM twice after the merge, so main's line stays. TestIsolationTests now also checks pending-start, and no test this PR adds builds a path under the real home.

Merged origin/main (24a26ff) in 2ccab01 after #47 and #38 landed. One conflict, in ReconcileLidGatingTests.swift: both sides added a line to the same fixture setup, so both stay (the fake reports SleepDisabled 1, and the fake process control reports both pids as stopped). #47's new testSessionWithOffsetDatesIsResumed expected a relaunch to run disablesleep 1 as main does, so a1f9f82 changes it to this branch's resume.

Merged origin/main (5e833d9) in ca56ec0 for #40. README.md merged without a conflict.

Merged origin/main (af9c4f6) in 12c8e40 for #30. README.md, docs/spec.md and docs/release-validation.md merged without a conflict, and #30 does not touch the sleep guard.

Merged origin/main (64886e9) in 3b1d726 for #22 (still-running privileged commands). Both sides kept: #22's SIGTERM-only runner with the recovery lock on the command's stdin, stopTransaction, the unfinished-command record, crash settlement and the launch retry, beside this branch's three-line rule, the dialog, the marker and reconcile's pmset -g read. PmsetSleepGuard takes main's init. Two StillRunningCommandTests about a sudo disablesleep 1 left running are dropped, because Start runs it through the dialog (the stuck-dialog tests cover that) and reconcile no longer runs it; the launch-retry tests expect reconcile's pmset -g.

Merged origin/main (c45e8fb) in 497cf15 for #28 (sealed backstop, installer supervisor). install.sh follows main's sealed layout, its independent supervisor with clock deadlines and its fixed tool paths. This branch's order stays on top: the password, the quit, the three-line rule written under the recovery lock with sudo -n and full paths, a sudo -k -n -l listing, then main's recovery and bundle swap. The older-backstop retire wait and the installer's pmset run are gone, because the sealed bundle has no separately installed backstop.sh to wait for. uninstall.sh keeps the pending-start deletion with main's sealed selection. A stop between the rule and the new bundle prints the rerun command.

Merged origin/main (aed25a5) in 6c02da5 for #49 (lid close leaves meeting apps running). Two conflicts, both resolved by keeping both sides. In SessionManager's end notification bodies, #49's backstop text, which depends on outputs still waiting for their audio restore, sits beside this branch's recoveryUnavailable, startFailed and sleepReenabled texts. Spec section 9 lists this branch's prompt notifications and #49's one-time lid-close settings notification. #49's per-device audio entries, save IDs, locked reconnect and lid gate, warning recovery and retained-session retry are unchanged. #49's testADeviceChangeBeforeTheLaunchReconcileWaitsForTheLidOfTheSessionOnDisk resumed a session from disk without setting the fake's SleepDisabled to 1; on this branch reconcile reads that setting instead of turning sleep off again, so the test now seeds it, as the other reconcile tests here do (fa281c1).

Merged origin/main (781b596) in 8f8693e for #33 (release pipeline). Both sides kept. From #33: the prebuilt --app install (signature, identifier, version and sealed backstop checked on a private copy before the password prompt, the arm64 check and --allow-unverified-origin), build-app.sh for source builds, ditto staging that drops group and other write bits and ACLs, and SCRIPT_DIR in place of ROOT. From this branch: the order (the password with sudo -v, the quit, then the three-line rule under the lock). Conflicts were in install.sh, README and release-validation.md:

  • A staging failure at step 3 says nothing was changed, because on this branch the rule is not written until step 5.
  • When the rule is written but the new bundle is not in place, the rerun line names the same bundle source (--allow-unverified-origin --app <path> for a prebuilt bundle).
  • The README keeps Release: build-app.sh, verified --app installs, and a release workflow #33's release-zip install section with this branch's three-command wording. README and SECURITY.md no longer say "four pmset commands".
  • Release: build-app.sh, verified --app installs, and a release workflow #33's prebuilt and source install tests marked the password prompt as sudo visudo. On this branch the prompt is sudo -v, and visudo runs later under the lock, so the tests mark sudo -v. The fake sudo now swaps the --app bundle during sudo -v, which falls between the checks on the private copy and the staging copy, so the swap test still shows that the checked copy is what gets installed.

Merged origin/main (bfc9a57) in 70f5fac for #50 (the supervisor owns each undo command's limit and signal). One conflict, in backstop.sh's lock_shared check. It keeps this branch's inode helper through "$STAT" (/usr/bin/stat) with #50's comparison, so a stat on PATH cannot make a foreign fd 9 look shared, and a shared fd 9 still skips the stale status-file cleanup that would delete a live supervisor's files. Everything else merged cleanly. #50's supervise_command and run_bounded sit beside this branch's marker, nonce, deadline, --own-bundle and pending-start handling: the supervisor owns its job, ignores TERM and HUP, keeps fd 9 until it reaps the command, sends TERM only, and on 125 keeps state and stops. Both sides' RecoveryScriptTests additions stay, and testLockSharingIgnoresAStatOnPATH (round 16) covers the kept "$STAT".

origin/main had not moved (bfc9a57), so 576c215 and 7dcf51f needed no merge.

origin/main had not moved (bfc9a57), so c5456f8 needed no merge.

origin/main had not moved (bfc9a57), so bd7db43 (bd7db43) needed no merge.

Merged origin/main (b5f7cf0, #43) with a merge commit, 15c2fc3, after the round 24 fixes in 7e3ddc8; no rebase and no force push. Four conflicts, each resolved by keeping both sides: SessionManager.init keeps this branch's markerLockTimeout and receiptLockTimeout and #43's keptRecheckDelay, keptRecheckAttempts, keptRecheckSlowDelay and bootSession; RuntimeState.CodingKeys has #43's kept-display keys and this branch's sleepOffAttempt; the backstop patch map in RecoveryScriptTests has this branch's CAT, HEAD, RECEIPTS and RECEIPT_OWNER and #43's MV; TestSupport.makeManager takes both sides' parameters. Two of #43's test files needed this branch's contracts, which no textual conflict showed. PrivateDisplayGuardTests' sleep-guard wrapper now forwards checkSleepSettingForStart, disableSleep(_:) and enableSleep() in place of setSleepDisabled, and its direct SessionManager call passes the harness's receipts (in 15c2fc3; the first merged build failed on both). In e741a7f, EarlierBootLowPowerClaimTests seeds a still-valid session with SleepDisabled 1 in the fake pmset, because this PR's relaunch never turns sleep off again and ends a session whose sleep was turned back on. In backstop.sh and uninstall.sh, #43's record_text_problems checks the journal before a settlement republishes state.json, a settlement runs before #43's kept-brightness handling, and uninstall.sh stops while an attempt stays, so a kept state.json never holds one. Main's own bare head and cat calls, among them #43's head -c 1 checks in prepare_low_power_off and the cat "$SESSION" session read, are left as main has them; finding 6 covers the calls this PR added.

origin/main had not moved (b5f7cf0), so 93fb3dc (93fb3dc) needed no merge.

origin/main had not moved (b5f7cf0), so d933b68 (d933b68) needed no merge.

origin/main had not moved (b5f7cf0), so f643d49 (f643d49) needed no merge.

Not covered

  • Everything in the eleven new "Not run" rows in docs/release-validation.md: force-quitting Insomnia with the dialog up, then relaunching it or waiting for the agent, then answering the old dialog (no SleepDisabled 1), a stuck osascript on real hardware (the fakes are the only coverage), the installer's session line and "Continue?" question in a real terminal, a cancelled installer password during a session leaving the app and session running, an upgrade whose app refuses to quit leaving the sudoers file as it was, an upgrade stopped after the rule printing the rerun command, the dialog on real hardware (its wording and app name), cancel and wrong password and the 120 s timeout rolling back, whether SIGTERM to osascript closes the dialog, relaunch with sleep still off keeping the session, relaunch after disablesleep 0 by hand ending it, and a reinstall over the maintainer's existing four-line file.

  • The sudoers block is also being changed by the installer-identity PR (Install: build the sudoers rule from the account, show it, and confirm #20). It builds the file from a list of four commands and compares the installed file against that list, so whichever PR merges second has to drop /usr/bin/pmset -a disablesleep 1 from that list; the rest of this PR's install.sh change (the heredoc) goes away in favour of Install: build the sudoers rule from the account, show it, and confirm #20's generator. Sleep guard: never SIGKILL sudo pmset; keep the lock while it runs #22 (sudo SIGKILL) edits sudoPmset and CancellableCommand, which this PR does not touch, but it still calls setSleepDisabled(true) in performStart and in reconcile, and its tests use the old name. Whichever of Sleep guard: never SIGKILL sudo pmset; keep the lock while it runs #22 and this PR merges second must use disableSleep(_:) after writing the marker for Start, enableSleep() for turning sleep back on, and drop the reconcile re-apply, which this PR replaces with the pmset -g read. This PR now also puts sudo -v and the session question ahead of the quit and the quit ahead of the sudoers step, and Install: build the sudoers rule from the account, show it, and confirm #20 (sudoers block) and Install, uninstall: identify a running Insomnia by path or bundle id, not by name #18 (how a running Insomnia is detected) edit both steps, so install.sh and the installer tests will need a hand merge against whichever lands first; the rule to keep is that no path writes disablesleep 1, the password comes before the quit, the app is quit before the rule is written, and the rule is written under the recovery lock after the retire wait. Install: build the sudoers rule from the account, show it, and confirm #20's --yes flag should also answer the session question.

  • The earlier narrow case (an old command's second read seeing a newer nonce and turning sleep back on) no longer exists: there is no second read. What stays: a wrong password, a timeout or a pmset failure still runs disablesleep 0, because pmset may have run; only a cancel and a launch failure are known to have run nothing. Since the fifth round, Start is refused while another tool's SleepDisabled 1 is set, so that undo can clear only a 1 set while the dialog was up. A root pmset that holds the marker lock longer than 10 s keeps the sleep entry journaled and refuses starts until it exits; nothing kills it.

  • The lock tests run lockf and the root command as the user, not as root, and the immutable-marker path is covered with chflags uchg in a temp dir; the hardware rows cover both on a real install.

  • No test runs the real osascript or shows a real dialog; the root command runs as the user with a fake pmset, and the dialog path on hardware is in the new "Not run" row.

  • Third round: everything in the new "Not run" rows (a stuck osascript rolled back at once on real hardware, a one-minute session answered after 70 s, an older backstop.sh refusing Start, an upgrade replacing backstop.sh before the bundle). The retire wait recognises old runs by their exact arguments in pgrep -lf output (fourth round); a run started through a different path to the same file, or with a different interpreter path, would not be seen. Bare rm, rmdir, mkdir, cp and mv calls that run as the user and are already on main in backstop.sh and uninstall.sh (including App Nap: opt in, journal the previous value and put it back #27's new EXIT trap in uninstall.sh) are unchanged; this PR adds none, and converting them would widen the conflict with Backstop: run only the copy sealed in the signed bundle #28. The two that ran as root through sudo are converted (afc4948). A voided prompt that the user never closes stays on screen until they do; it can no longer change anything, and the menu names it.

  • Fourth round:

    • Deleters that did not write the marker (backstop.sh, uninstall.sh, and an app transaction after a relaunch) have no identity to compare. A copy swapped in before their open, while the root command holds the original, would be deleted in its place. Only a process running as the user can make that swap, and it can already clear sleepDisabledByUs in state.json. A start voiding its own stuck prompt does compare against what it wrote.
    • The installer's last look for a running app comes just before rm -rf of the bundle. An app opened in between is not seen, and the installer never kills it.
    • The new "Not run" row (Start with the sudoers file moved aside) and the changed one (an upgrade stopped before the rule).
  • Fifth round:

    • The restore check reads pmset -g and then runs the restore. A tool that sets SleepDisabled 1 between the two has it turned back on by the check. pmset has no compare-and-set that would close the window.
    • An install while SleepDisabled reads 1, or while pmset -g fails, finishes without checking the rule. It says so, and the app checks before every Start.
    • The three new "Not run" rows. No test runs the real sudo, so its answers to -k -n under a listing-only policy or with a cached credential come from the fake sudo, not from sudo itself.
  • Sixth round (stated for the maintainer, not accepted on their behalf):

    • pmset has no compare-and-set. A SleepDisabled 1 another tool sets after Start's pmset -g read is not seen. The session's end sets it to 0, and if the tool sets it while the dialog is up, the root command's check sets it to 0 for a moment before setting 1. Nothing is left unjournaled, and the check never makes a change the end or a rollback would not make, but that tool's setting is not kept. This is what remains of Greptile 4171743074. README, SECURITY.md and the spec say so.
    • The user types the password before a missing rule is reported.
    • A Mac whose sudoers has no entry for root refuses every Start after the password.
    • Between the root command's last clock read and pmset -a disablesleep 1 there is still the time pmset takes to start. A session that ends in it ends at once, because the start arms its deadline timer next and a timer for a past date fires immediately.
    • No test runs the real sudo or runs the root command as root. The fake sudo models root's password-free -u and the user's -k and -n. Two new release-validation rows cover the real ones.
  • Upgrade fix for 6046657261:

    • --own-bundle checks the shape of the script's path, not the bundle's signature. It relies on install.sh running only a copy codesign has verified. Someone who runs another bundle's copy with the flag gets that bundle's binary, run as the same user who could run that binary directly.
    • A prebuilt bundle downloaded in a browser keeps its quarantine attribute in the private copy and the staging copy (as Release: build-app.sh, verified --app installs, and a release workflow #33 intends). Whether macOS lets the backstop run that staged binary was not tried. If it is blocked, the binary gives no valid answer, so the entries stay and the install stops before the swap with the previous app and agent in place. A rerun would stop the same way. Only an upgrade with microsecond frozen entries in the journal runs the binary at all.
    • No real upgrade was run. The new release-validation row covers it, from a source build and from a quarantined download.
  • Round 16, the round 14 P1. This is narrowed, not closed, and needs the maintainer's decision; it is stated here, not accepted on their behalf:

    • pmset has one SleepDisabled bit with no owner and no compare-and-set. A 1 another tool sets after the root command's first read and before its own disablesleep 1, or while that 1 is in effect during the proof, cannot be told from Insomnia's own. The proof, root's restore after a failed proof, or the session's end clears it. That window runs from the read to the proof's write, including however long the user's sudo takes; it was not measured on hardware.
    • The ambiguous failures (.failed, .timedOut, .stillRunning) and crash recovery still undo the entry, because pmset may have run, so they can clear a 1 set while the dialog was up. While a session owns the bit, its end clears a 1 another tool set. Both are as before.
    • Gone in round 18, which writes nothing before sudo answers: New cost: sleep is off for as long as the proof runs, also when the rule is missing. If the root shell dies in that moment (power loss, or root killing it), sleep stays off with the journal entry in place, and without the rule the end, the backstop and uninstall cannot turn it back on without the user. The new Not run row covers the moment on hardware.
    • Closing the window needs a sleep assertion owned by a process, which ends with it, or a compare-and-set write. Both need a privileged helper, which Option 2 rules out. The round 17 review found that choice premature, because sudo's own answers can check the permission without a write; round 18 does that, and what remains is below.
    • The fake sudo models the cached-credential, listing-only, PASSWD, deny, no-rule and no-root-entry policies; the real sudo never runs. The hardware rows cover the real ones.
  • Round 18, the round 17 findings. Stated for the maintainer, not accepted on their behalf:

    • R1 is removed for the cases the review reproduced: no write comes before the three answers, so a refused or interrupted check leaves sleep as it was. A listing is not the restore: a rule removed after the check, a log sudo cannot write when the restore runs (log_output set some way the listing does not show), or other groups for the user when the app or backstop.sh runs sudo than when root switched to that user can still make a later restore fail, and backstop.sh then keeps the entry and retries. Round 20 refuses log_output and every other Defaults entry outside its list (below).
    • R2 is narrowed, not closed. pmset has one SleepDisabled bit with no owner and no compare-and-set. A 1 another tool sets between root's read and its write is taken for Insomnia's, and the session's end clears it. Between that read and the write there is now only the clock comparison, not a sudo run; the moment was not measured on hardware. A 1 set while a session owns the bit is cleared at its end, as on main.
    • An exit status lost to a crash or a signal (lockf 70, a timeout, a stuck prompt) is still undone like an end, as on main, because a write may have happened, so it can clear a 1 set while the dialog was up. The SIGPIPE case no longer gets there. Round 20 narrows this with the marker's record (below).
    • A refusal while the journal already owns a 1 ($5 = "1") leaves that 1 and the owed restore in place, which the next end or backstop.sh run settles.
    • Every Start refuses, after the password, on a sudo outside 1.9.15 to 1.9.x, other plugins, Defaults bound to a Runas user or a command, listpw=always, a sudoers with no root entry, or a later rule for the restore. The message names these. Round 20 takes only 1.9.17p2 and adds refusals (below).
    • No query has its own time limit. One that hangs is bounded by the dialog's 120 s limit, after which the start keeps the lock until the command exits, as for any stuck prompt. Corrected in round 20, after the round 19 review: the 120 s limit bounds the app's wait, not the command. A query that never returns keeps the marker's lock, and the start keeps the recovery lock and session.json, until it exits; nothing writes after the lock is released.
    • install.sh still checks only the sudo -k -n -l listing of the three commands; the root command's check runs at every Start.
    • The fake sudo's answers come from sudo's source, not the installed sudo. The version check takes 1.9.N for any N of 15 or more; the sources read are 1.9.15, 1.9.16, 1.9.17p2 and Apple's sudo-114.100.11, so a later 1.9 release is taken on the assumption that its answers keep this format, and one that changed them would refuse. The two new Not run rows cover the real output and the two refusals. Round 20 takes only 1.9.17p2.
  • Round 20, the round 19 findings. Stated for the maintainer, not accepted on their behalf:

    • F1 and F2 are closed for the setups the review built, by refusing them, and by refusing the classes they belong to: any sudo.conf, a PAM session stack other than macOS's own, any sudo but 1.9.17p2, and any Defaults entry outside the list. The list and the version come from Apple's source, not from running the installed sudo. A listing is still not the restore: a rule, group, sudo.conf or PAM change after the check, or groups for the user under the app or backstop.sh other than the ones root's sudo -u gives, can still make a later restore fail, and backstop.sh keeps the entry and retries, as on main.
    • New costs: after the password, every Start refuses on any sudo but 1.9.17p2 (a macOS update with another sudo refuses every Start until a release is checked against it), on any /etc/sudo.conf, a comment-only one included, on any PAM session line other than macOS's own, and on any Defaults entry outside the list. Each Start makes one more sudo call, for the record, and there is a new refusal, 7. Round 22: root writes the receipt itself, with no sudo call, and 7 now means a receipt that is unsafe or cannot be written (below).
    • F3 (ownership) is narrowed, not closed, and stays unwaived. A failure before the record no longer clears a 1 another tool set while the dialog was up: a wrong password, a timeout before the command ran, a stuck prompt whose command never got that far and a signal while sudo is asked now leave it, as every refusal already did. These remain:
      • After the record, a failure before the write is still undone like an end, and clears such a 1 (testAnAmbiguousFailureAfterTheRecordStillUndoesASettingMadeMeanwhile, testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite). The record cannot be written in the same step as pmset's write, so between root's read and the end of that write a failure cannot say which side it fell on. The window is the read, its awk and one clock comparison, and was not measured on hardware. Round 22 moves the record after the read (below).
      • A 1 another tool sets between root's read and its write is taken for Insomnia's and cleared at the end. pmset has no compare-and-set.
      • A 1 another tool sets while a session owns the bit is cleared at the end, as on main.
      • A crash or force-quit with the dialog up is recovered by relaunch, backstop.sh or uninstall.sh, which do not read the record, so the entry is undone as before round 20. Round 22: they settle from the receipt (below).
      • Any process running as the user can write the marker. Erasing the record only brings back the undo. Writing the bare nonce back after the write, when that start then fails, makes the app roll it back without the undo, which leaves sleep off with no journal entry. Before round 20 every such failure ran the undo. Such a process can already clear sleepDisabledByUs in state.json. Round 22: the record is in the receipt, which such a process cannot write (below).
    • Bounded alternatives for the maintainer, none taken here: a receipt in a folder install.sh creates owned by root, which a same-user process cannot write (one more install step and a root write per Start); the nonce and record in the journal, so relaunch and backstop.sh can read them with identity checks like the app's; or accepting the cases above as Option 2's limit. A sleep assertion held by a process or a compare-and-set write would close the first two, and both need the privileged helper Option 2 rules out. Round 22 takes the first two together (below).
    • The record is not fsynced. Only the running app reads it, through the page cache; after a power loss, recovery does not read it anyway. Round 22: the receipt is written with fsync(2), and recovery reads it (below).
    • The two new Not run rows and the three changed ones cover the real sudo, PAM and dialog. No test ran the real sudo, so the Defaults list and the version are checked only against source and fakes.
  • Round 22, the round 21 findings. Stated for the maintainer, not accepted on their behalf. F3, the ownership finding from rounds 14, 17 and 19, is narrowed again and stays open and unwaived.

    • Corrected here: the same-inode forgery (the marker is no longer evidence, and nothing running as the user can write the receipt or a folder above it); relaunch, backstop.sh and uninstall.sh after a crash with the dialog up, expired or not (they settle from the receipt, never resume that session, and keep an earlier owed restore); a stop at root's read (now before the record, so the 1 stays).
    • Still unsafe, from this PR's design. After <nonce> writing, a failure before pmset's write ends (a signal, a crash, pmset failing, the app's timeout after the password) is undone like an end. Trigger: another tool sets SleepDisabled 1 after root's pmset -g read, then the command fails or is stopped before its write is known. The restore clears that 1. The window holds the read's awk, the dd write with fsync, the readback and one clock comparison; it was not measured on hardware.
    • Still unsafe, from this PR's design. A 1 another tool sets between root's read and root's write, when the start then succeeds, is taken for Insomnia's and cleared at the end. pmset has one boolean with no owner and no compare-and-set (Apple's pmset writes it through IOPMSetSystemPowerSetting), so no receipt can tell these two histories apart.
    • Inherited from main. A 1 another tool sets while a session owns the bit, or while the journal owes a restore from an earlier session, is cleared by the end or by recovery. Main does the same.
    • New in round 22, durability. fsync(2) is not F_FULLFSYNC and no tool the command runs can ask for that, so after a power cut the receipt can come back with its older content while pmset's write survived. That reads as "never": the journal entry goes, and reconcile reports the 1 as set by something else, with the command that clears it. A torn receipt reads as "may have". state.json and session.json are written by rename without fsync, as on main.
    • New in round 22, settlement failures. A settlement that cannot be written restores sleep even when the receipt showed no write, and so can clear another tool's 1. A session whose marker or attempt record could not be cleared after it started ends early at the next relaunch or backstop run.
    • A process running as the user. It can still rewrite state.json, including sleepOffAttempt, which on main was already enough to drop an owed restore. The marker's lock is advisory, so it can delete or replace the marker while a root command holds it. A live start then waits for that command, because its removal expects the file it wrote. A relaunch, backstop.sh or uninstall.sh reads that as "may have" and restores, and a command still running can then write after that restore, leaving sleep off with no journal entry. If it recreates a settled start's marker with the old nonce and the user then types the password into that start's old dialog, sleep goes off with no journal entry, and reconcile reports it as set by something else.
    • The receipt checks require /, /private, /private/var and /private/var/db to be root's with no group or other write permission and no access control entry that allows anything. That was read from no real Mac in this round (no system path was inspected); if a stock Mac differs, install.sh stops at the receipt step and every Start refuses. The new Not run rows cover it.
    • Bounded alternatives for the remaining phases, none taken here: accept the read-to-write moment and the owned-session clearing as Option 2's limit; change the product to an assertion-only awake mode, which never writes the global bit but changes lid-closed and battery behavior and needs the maintainer's scope decision; or an OS operation that knows who set the bit, which was not found. A helper making the same pmset writes adds no ownership. F_FULLFSYNC would need a privileged binary of Insomnia's own, which Option 2 rules out.
    • The round 20 Tests section above names two hand mutations of the marker rule. Their saved diffs hold only 12-byte closing fragments, not the mutants, so those results cannot be checked from the evidence. The marker rule they mutated is gone in round 22, and they were not rerun.
    • No test runs the real sudo, osascript, pmset, install.sh or uninstall.sh, or writes under /private/var/db. Every receipt in the tests is a file in a temporary folder that the test user owns, trusted only through the test-only constructions above.
  • Round 24, the round 23 findings. Stated for the maintainer, not accepted on their behalf. Finding 7, the ownership finding from rounds 14, 17, 19, 21 and 23, stays open and unwaived. pmset's disablesleep is one Boolean with no owner and no compare-and-set, so in each pair below two different causes leave the same bit, receipt and journal. The tests that show another tool's 1 being cleared describe this finding; they are not guards.

    • Still unsafe, from this PR's design: a recorded write that is not known to have happened. Trigger: another tool sets SleepDisabled 1 after root's pmset -g read. Then, before Insomnia knows pmset wrote, the command fails, is signalled or crashes, osascript's status is lost, the app's 120 s wait ends, or the app quits. Once the command lets go of the receipt, every reader reads this nonce's writing as "may have written", and the undo clears the other tool's 1. A1 (pmset wrote 1 and the status was lost) and A2 (the command stopped before pmset and another tool set 1) both leave writing and a 1. The window runs from the read through the perl write, its F_FULLFSYNC and the read-back to the end of pmset. That is wider than round 22's dd ... fsync by the drive flush, which has not been measured on any Mac. testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite shows the loss.
    • Still unsafe, from this PR's read: the gap between the read and the write. Root reads 0, another tool sets 1, root writes writing and runs disablesleep 1. The session claims the bit, and End clears the other tool's 1. B1 (Insomnia's 1 alone) and B2 (another tool's 1 in the gap, then Insomnia's write) both end with writing, success and a 1. The F_FULLFSYNC write is inside this gap, so round 24 likely made it longer; not measured. testASettingMadeRightAfterTheCommandsReadIsClearedOnlyWhenTheWriteFollows shows the loss.
    • Inherited from main. A 1 another tool sets while a confirmed session owns the bit, or while the journal owes a restore (then $5 is 1 and root skips the read), is cleared by the next End, reconcile or backstop run. C1 (Insomnia's 1) and C2 (Insomnia's 1 plus another tool's) look the same. Main does the same, and that is no reason to accept the two cases above.
    • Other cases that clear another tool's 1. A "may have written" settlement that cannot be written restores and keeps the attempt. After expires, a missing, replaced, unsafe or malformed receipt reads as "may have written"; only root or an administrator can change the receipt or its folders. A marker with no journaled attempt ends its session (the legacy rule). A process running as the user can rewrite state.json or session.json (inherited).
    • Minimal alternatives, none taken, for new independent review and a later choice by the maintainer:
      1. Read pmset -g again right after writing and refuse on a 1. Both cases above shrink to the read, the expires check and pmset's start. Cost: one more F_FULLFSYNC write on that refusal, and if it fails, writing stays and the 1 just seen is cleared. It narrows the gap and does not give ownership.
      2. On "may have written", keep the attempt, run no automatic restore and tell the user to run sudo pmset -a disablesleep 0. Cost: a possible Insomnia 1 stays past the session's end, so the Mac does not sleep until someone acts, against the deadline promise and the backstop's job.
      3. An assertion-only awake mode, which never writes the global bit. Cost: lid-closed and battery behavior change, and the deadline needs its own enforcement. Not approved.
      4. Accept the two cases above as Option 2's documented limit. This needs an explicit waiver; none has been given.
      5. A privileged helper writing the same Boolean, or a compare-and-set at Start. Neither tells C1 from C2 at restore. Not approved.
    • Costs added in round 24. Start needs /usr/bin/perl; Apple's Catalina release notes say future macOS versions will not include scripting runtimes by default, and without perl no session can start. F_FULLFSYNC's effect on real drives, the real /private/var/db folder modes and a power cut are not measured. Any local account can hold the receipt's lock, which keeps an attempt undecided and Starts refused but cannot make a write look absent. An unsettled claim from a folder whose agent never runs refuses Start in every other folder until that folder's app or backstop settles it, or someone removes both files with sudo rm -f and runs install.sh again (SECURITY.md). A timed-out dialog can keep Start refused for the rest of its 130 s window.
    • Not changed: main's own bare head and cat calls in backstop.sh and uninstall.sh, state.json and session.json written without fsync, and the legacy marker rule. A bd7db43 development build's 45-byte receipt and attempt record are not migrated; install.sh stops at the receipt.
  • Round 26, the round 25 findings. Stated for the maintainer, not accepted on their behalf. F7, the ownership finding (R25-5), stays open and unwaived. The second read narrows two windows; it is not an owner token and does not close them. In each pair below, two different causes leave the same bit, receipt and journal. The tests that show another tool's 1 being cleared describe this finding; they are not guards.

    • Still unsafe, from this PR's design: a recorded write that is not known to have happened. Root's status is lost (the app quits or crashes, osascript's answer is lost, or the 120 s wait ends) while the receipt shows this nonce's writing and SleepDisabled is 1. A1: pmset wrote Insomnia's 1. A2: the command stopped after writing and before pmset (a signal, a crash, a power cut), and another tool set 1. A3: the second read saw another tool's 1, the refused over the record could not be written, and status 6 never reached the app. Every reader reads writing as "may have written" and the undo clears the other tool's 1 in A2 and A3. Before round 26 a 1 set at any time from the first read on counted for A2; now the command refuses on a 1 it sees at the second read, so A2 needs the 1 to come after that read or the command to stop before it. testACommandStoppedAfterItsRecordIsUndoneEvenBeforeItsWrite and testASecondReadRefusalWhoseRefusedLineCannotBeWrittenLeavesTheRecord show the loss.
    • Still unsafe, from this PR's read: the gap between the last read and the write. B1: Insomnia's 1 alone. B2: another tool sets 1 after root's second read, root passes its clock check and runs disablesleep 1. Both end with writing, success and a 1; the session claims the bit and End clears the other tool's 1. The gap is now the clock check (/bin/date) and pmset's start; it no longer contains the perl write and its F_FULLFSYNC. Not measured on any Mac. testASettingMadeRightAfterTheCommandsReadIsClearedOnlyWhenTheWriteFollows and testWhereAnotherToolsOneLandsDecidesWhetherItSurvives show the loss.
    • Inherited from main and unchanged. A 1 another tool sets while a confirmed session owns the bit, or while the journal owes a restore (then $5 is 1 and root skips both reads), is cleared by the next End, reconcile or backstop run. C1 (Insomnia's 1) and C2 (Insomnia's 1 plus another tool's) look the same.
    • Smallest alternatives, none taken, for the parent's review and a later choice by the maintainer:
      1. A third receipt state written after pmset returns, written (seven letters, so the line stays 82 bytes), flushed the same way, and writing without it read as "never wrote". A1 with a lost status then reads written and is undone; A2 and A3 read writing and keep the other tool's 1. Cost: a crash, signal or power cut after pmset wrote and before written is flushed leaves Insomnia's 1 with a journal that says it never wrote, so the Mac does not sleep until someone runs sudo pmset -a disablesleep 0; another backstop version and receipt-reader change; one more F_FULLFSYNC per Start. B2 is unchanged.
      2. On "may have written" from a lost status, keep the attempt, run no automatic restore and tell the user to run sudo pmset -a disablesleep 0. Cost: A1's Insomnia 1 stays past the session's end, against the deadline promise and the backstop's job.
      3. An assertion-only awake mode, which never writes the global bit. Cost: lid-closed and battery behavior change, and the deadline needs its own enforcement. Not approved.
      4. Accept A2, A3 and B2 as Option 2's documented limit. This needs an explicit waiver; none has been given.
      5. A privileged helper writing the same Boolean, or a compare-and-set at Start. Neither tells C1 from C2 at restore. Not approved.
    • Costs added in round 26, not waived. An undecided start now holds the sleep undo for as long as the receipt stays locked. The command holds that lock until pmset exits, so a command that never exits keeps sleep as it is (possibly off) and Start refused indefinitely; Insomnia never kills it. The receipt is root's file with mode 0644, so any local account can open it and hold its advisory lock with the same effect; round 24 already let that keep Start refused, and now it also holds the sleep undo after expires. A settled record that cannot be finished (a release file not as install.sh made it, an immutable state.json, a busy lock) keeps Start refused in its folder, and its held claim refuses Start in every folder of the user, until a run finishes it; a relaunch then ends an unexpired session rather than resume it. install.sh now stops at a held claim in a release file that is not the user's 0600 file with one link; removing both files by hand (SECURITY.md) is the way out.
    • Boundaries. "Never wrote" from expires rests on the wall clock not going back: root's clock check uses /bin/date, so after a clock is set back, a later answer to a dialog settled that way passes the clock check; the marker check (exit 3) and the predecessor check (exit 8) still apply. After expires, a missing, replaced, unsafe or malformed receipt still reads as "may have written". The receipt's F_FULLFSYNC flush is not atomic with state.json, session.json or pmset's own settings; a power cut between them is not tested. The receipt write needs /usr/bin/perl; the 45-byte receipt of bd7db43 is refused, not migrated; sudo, PAM or policy refusals stop Start. The window sizes above, the real folder modes and real drives were not measured.
    • CI. The hosted Swift job hit its 1200 s watchdog on e741a7f with no failed case. This round adds 20 tests (64.1 s in the local full, which took 1546.9 s against 1221.3 s on e741a7f, partly under load) and finds no fixture saving that covers the gap, so the job is likely to hit the watchdog again. No rerun, timeout change or matrix cut was made.
  • Round 28, the round 27 findings. Stated for the maintainer, not accepted on their behalf. F7, the ownership finding (R27-1), stays open and unwaived. pmset's disablesleep is one Boolean with no owner and no compare-and-set.

    • What changed for F7. Before, an app that got exit 6 (the second read saw another tool's 1), where the root command could not write refused over its record, rolled back with no undo, but when it could not journal that rollback it kept the attempt, and its next transaction read writing again and cleared the other tool's 1. Now that app keeps the nonce in refusedNonce, and its later settlements of the start keep "never wrote" whatever the receipt shows. testARefusalWhoseRollbackCannotBeJournaledStaysARefusalInThisProcess covers it; with the retention removed it fails at the same-process undo.
    • Still open. A1 (Insomnia's 1 with a lost status, restored correctly) and A2 (the command stopped after writing and before pmset, and another tool set 1 after the second read or the command stopped before it) read the same, and the undo clears the other tool's 1 in A2. A3 (the second read refused another tool's 1 and refused could not be written) is now cleared only when the status is lost (a signal, the 120 s limit, a crash before the app acts on it), when the app quits or crashes before it can journal the rollback, or when backstop.sh or uninstall.sh settles the start first. B2 (another tool's 1 after root's second read and before its clock check and pmset) reads as a successful Start, and End clears it. C1 and C2 (an owned session's 1, or that 1 plus another tool's) are inherited from main. The tests that show another tool's 1 being cleared describe this finding; they are not guards.
    • Considered and not done: retrying the refused write inside the root command (helps only a passing write failure and changes the root text in both copies), and publishing the negative verdict outside state.json (the only other files the app may write are the user's own, every reader would need a new format, and it fails under the same storage faults).
    • Alternatives, none taken, for the parent's review and a later choice by the maintainer: 1. Keep the behavior and state it; this needs an explicit, narrow acceptance, and none exists. 2. A written receipt state after pmset returns, with writing alone read as "never wrote"; it fixes A2 and A3 for every reader, but a crash after pmset wrote and before written is flushed leaves Insomnia's 1 with a journal that says it never wrote, so the Mac does not sleep past the deadline. 3. No automatic restore on "may have written" from a lost status; A1's 1 then stays past the deadline. 4. An assertion-only awake mode; lid-closed, battery and owner-death behavior change. 5. A helper or a compare-and-set at Start; new privileged code that still cannot tell C1 from C2. Options 3 to 5 are not authorized.
    • Costs, not waived. A privileged command that never exits keeps the receipt's lock and its folder's recovery lock (SIGTERM only, never SIGKILL): sleep stays as it is past the deadline, the start stays undecided and Starts are refused in every folder of the user, with no limit. The installing user, and anything running as them, can still open the receipt and hold its lock with the same effect; other accounts no longer can. A settled record that cannot be finished keeps Starts refused in its folder and its held claim refuses Starts in every folder of the user; its own healthy session now resumes meanwhile. Uninstall now stops while another folder holds a claim, and a claim from a folder that is deleted or whose agent never runs stops it until both files are removed by hand. Uninstall cannot see another folder's journal, so removing the shared rule can still strand that folder's owed restore once its claim is free; that is the machine-wide sudoers transaction PR Install, uninstall: identify a running Insomnia by path or bundle id, not by name #18 owns, not copied here.
    • Limits of this round's changes. The shell checks see the receipt's ACL as ls -le prints it: ls never prints the synchronize right, prints folder-only rights and inheritance flags only for folders, and skips an entry it cannot translate, so a receipt whose only extra is one of those passes the root command, backstop.sh, uninstall.sh and install.sh; the app's acl(3) check is exact. install.sh repairs an earlier receipt before it takes the receipt's lock (the user cannot open a 0600 receipt with no entry), so for that moment every reader refuses it; and an older build still running in another folder reads the repaired receipt as unsafe, so a start it left open across the upgrade settles as "may have written" once expires has passed. Not tested across builds. backstop.sh's type_of and extract still read a failed plutil call as absence; only its raw reader and its conversion report failure. Bash may not report an error partway through $(<file). The healthy-session match allows 1 s, which grows with relaunches between clamped extensions; past it the session ends as before. Left as bare calls, none reading the journal, the session or a power state: heredoc cat that prints a message, install.sh's sed | head -n 1 over codesign output, uninstall.sh's awk over App Nap IDs and backstop.sh's grep -q over a probe file.
    • Boundaries. No real chmod +a, ACL, sudo, pmset, install or uninstall ran; the ACL tests use stand-in lists and a fake ls, and the Apple sources read are chmod.1, chmod_acl.c, ls.1 and ls's print.c. "Never wrote" from expires still assumes the wall clock does not go back. The receipt's flush is still not atomic with state.json, session.json or pmset and is not measured on real drives.
    • CI. At 93fb3dc the hosted Swift job hit its 1200 s watchdog with RecoveryScriptTests at 921.3 s. This round adds 34 tests (122.7 s in the local full, which took 1579.4 s against 1546.9 s at 93fb3dc) and saves about 24 s in AppEncodedJournalScriptTests. Local time alone does not show whether the hosted job fits. Hosted run 37873337464 at d933b68, one snapshot at 02:23:07Z: ShellCheck scripts and Lint workflows passed; the Swift test and release build job was PENDING (its test step still running, release build and lid check not started); Greptile Review was in progress. No workflow, watchdog, job limit, partition or rerun change; the review's two-job split is the parent's decision.
  • Round 30. Stated for the maintainer, not accepted on their behalf. F7 stays open and unwaived.

    • Costs added, not waived: uninstall's sudo -v has no time limit while it holds both locks; a credential that runs out after the bootout stops it there (rerun it); install.sh repairs nothing under a held claim and counts a missing release file as no claim; an older build refuses a repaired receipt, also after a rollback; a session a failed start put back beside a settled record ends at relaunch; the shell ACL checks pass rights ls -le does not print.
    • Full disk, as on main: a failed log append ends backstop.sh's run, possibly before the restore; a publish needs a new file; a journal with a NUL byte is left as is. A crash can lose uninstall's removal record (no fsync); the rerun then refuses.
    • Finding 3: a free claim does not show another Insomnia folder owes no restore. Once uninstall removes the rule, that folder needs sudo pmset -a disablesleep 0.
    • Finding 13: faster fakes and shorter polling were not done, so the hosted job likely still hits its 1200 s watchdog. Proposed, not implemented: split the 1551 IDs into 3 jobs by exact ID lists, about 520 s of cases each and 878 s of Run tests at main's slowest spread (watchdog 1200 s). Two jobs would reach 1263 s at that spread. The estimate and lists stay in Root's round 30 evidence (capacity/).
    • Kept as history, no longer true: the 0644 receipt any account can lock (rounds 24 to 26), a relaunch ending a settled start's session (round 26), the installer's sudo pid and sudo kill line (round 15).

Codex review

Local Codex review (gpt-6.1-sol, xhigh) of 71253d7. All three findings are fixed in a2ffdd4 (merged with main in b5f6de9).

  • [P0] AdministratorPrompt.swift:267: an abandoned password prompt could disable sleep after recovery had cleared the journal. Fixed: the pending-start marker and the nonce-checking root command (What, Decisions). The relaunch path is covered by testRelaunchVoidsTheDialogOfAStartThatDied and testDialogOfAStartThatDiedCannotActForANewerStart, the backstop path by testBackstopVoidsTheDialogOfAStartThatDiedBeforeItUndoesAnything, testBackstopVoidsAnAbandonedDialogEvenWhileTheSessionIsValid and testBackstopLeavesTheMarkerWhenTheLockIsHeld, the race with recovery by testMarkerDeletedWhilePmsetRunsTurnsSleepBackOn, plus uninstall, rollback and the stuck-prompt path. The sudoers rule keeps its three exact lines.
  • [P1] SessionManager.swift:1109: the menu kept recommending kill <pid> after osascript had exited while a descendant held its pipes. Fixed: the runner reports osascript's own exit to the handle as soon as it is reaped, separately from pipe closure; the start waits for that, replaces the line, and still waits for the pipes before the rollback. Covered by testOsascriptExitReachesTheHandleBeforeItsOutputCloses and the extended testStuckPromptIsReportedWithItsPidAndRolledBackAfterItExits.
  • [P2] AdministratorPromptTests.swift:156: the output-holder test depended on a scheduling window. Fixed: OsascriptAdministratorPrompt takes a beforeDeadline hook, the tests block in it until the fake has written a ready file after its TERM trap, and each holder runs until the test releases it (60 s watchdog). The SIGTERM and ignore-SIGTERM tests use the same handshake.

Second local Codex review (gpt-6.1-sol, xhigh) of b5f6de9. All four findings are fixed in b180529.

  • [P0] scripts/backstop.sh:129: recovery cleared the journal while the marker could not be deleted, and the app's clearPendingStart swallowed the same failure. Fixed: sleepDisabledByUs is cleared only by a run that removed the marker. Otherwise sleep is still restored, the entry stays, the app reports it ("Restore incomplete", menu line) and refuses starts, the backstop exits 1, and every run retries. Covered by testUndeletableMarkerKeepsTheSleepEntryAndRefusesStarts, testRelaunchWhileTheAbandonedDialogsCommandRunsKeepsTheSleepEntry, testBackstopKeepsTheSleepEntryWhenTheMarkerCannotBeDeleted, testBackstopKeepsTheSleepEntryWhileTheMarkerIsLocked and testBackstopFailsOnAStuckMarkerEvenWithACleanJournal.
  • [P0] AdministratorPrompt.swift:167: a failed compensating disablesleep 0 went unnoticed, and an ambiguous disablesleep 1 failure skipped compensation. Fixed by removing compensation rather than checking it: the root command holds a lockf lock on the marker from its check until pmset exits, and every deleter takes that lock first, so the marker can no longer go while pmset runs and whatever pmset did is still covered by the journal (Decisions). Covered by testTheMarkerCannotBeRemovedWhilePmsetRuns, testAnAnswerThatWaitsOnARemovalRunsNothing and PendingStartRemovalTests.
  • [P1] SessionManager.swift:424: cancelling Start ran disablesleep 0 and cleared a SleepDisabled another tool owned. Fixed: a cancel and a launch failure put session.json and the journal back exactly and run no pmset; a wrong password, a timeout and a pmset failure keep the undo path, because pmset may have run (Not covered). Covered by testCancelLeavesASleepSettingSomeoneElseOwns, testLaunchFailureRollsBackWithoutPmset, testCancelKeepsAnEntryAnEarlierRestoreLeft, testCancelTextInACommandsOutputIsNotACancel and testOnlyCancelAndLaunchFailureRanNothing.
  • [P2] AdministratorPromptTests.swift:158: the SIGTERM test depended on the 3 s default grace. Fixed: it passes a 20 s grace and accepts .stillRunning too, checking the reported grace and waiting on the handle before it reads the trace. With a 1 s grace the new test passes and the old one fails.

Third local Codex review (gpt-6.1-sol, xhigh) of b180529. All four findings are fixed in aced667 (merged with main in 178dde8).

  • [P0] scripts/install.sh:190: an upgrade that stopped before the backstop step left the new app beside an old backstop.sh, which does not delete pending-start, so a dialog left open by a crash could still turn sleep off. Fixed at both ends. backstop.sh declares # insomnia-backstop-version: 2, and Start refuses, with nothing written and no dialog, while the installed script is missing that line or is older, telling the user to run install.sh again. install.sh installs backstop.sh under the recovery lock before the bundle and waits for runs of the old copy to exit (Decisions). Covered by BackstopVersionTests, testStartWithAnOlderBackstopShowsNoPrompt, testInstallReplacesTheBackstopBeforeTheBundleUnderTheLock and the three retire-wait tests.
  • [P0] SessionManager.swift:448: a stuck osascript kept the recovery lock, and so blocked sleep restoration, even after its marker was gone. Fixed as directed: once clearPendingStart() has removed the marker under its lock, the start restores sleep, finishes the transaction and releases the lock, and a separate task keeps the menu line true until the prompt exits. A command that holds the marker's lock is still waited for. Covered by testStuckPromptWhoseMarkerIsGoneIsRolledBackWithoutWaiting (an osascript fake that never exits after the marker is voided), testVoidedPromptWatcherLeavesALaterLineAlone and testStuckPromptWhoseCommandHoldsTheMarkerIsWaitedFor.
  • [P2] AdministratorPrompt.swift:187: a password accepted after the session's deadline still turned sleep off. Fixed in the root command: the deadline arrives as $3 and the command refuses with exit 4 unless /bin/date +%s is below it, still as fixed text (Decisions). Covered by testDoesNothingOnceTheSessionHasEnded, testAnUnreadableDeadlineNeverPasses, testPasswordTypedAfterTheSessionsEndTurnsNothingOff and the argument test.
  • [P2] AdministratorPromptTests.swift:393: the removal test assumed lockf opened the marker within 300 ms. Fixed: waitUntilLockfWaits(under:) confirms that lockf is blocked in the kernel waiting for the marker's lock before the test deletes it (Decisions). 15 of 15 runs passed, and removing the wait fails the test.

Fourth local Codex review (gpt-6.1-sol, xhigh) of 887bc71. The one finding is fixed in 1b36a53.

  • [P1] SleepGuard.swift:60: Start could turn sleep off when the passwordless restore was missing, so the end, the backstop and uninstall would all fail to turn it back on. Fixed at the root: Start now refuses, before anything is written or any dialog is shown, unless sudo -n -l /usr/bin/pmset -a disablesleep 0 exits 0. The message says to run scripts/install.sh again. The check never prompts and never runs pmset, and the restore itself uses the same argument list. Covered by testStartWithoutThePasswordlessRestoreShowsNoPrompt, testThePasswordlessRestoreIsCheckedBeforeAnythingIsWritten and three fake-sudo tests. The fifth review replaced the listing with a run of the restore (below).

Fifth local Codex review (gpt-6.1-sol, xhigh) of f72ba11. The one finding is fixed in 926d639 (merged with main in ca56ec0 and 12c8e40).

  • [P0] SleepGuard.swift:95: a successful sudo -n -l did not prove that sleep could be turned back on without a password. An administrator account with another NOPASSWD entry, or with a cached credential, passed without Insomnia's rule, so Start could turn sleep off with no unattended way back. Greptile reported the same line (4171411041). Fixed at the root in the app and in install.sh, which had the same check: both run sudo -k -n /usr/bin/pmset -a disablesleep 0. The app runs it without a read when the journal owes the restore. Otherwise it reads pmset -g first, runs it only while SleepDisabled reads 0, and refuses Start with nothing run while another tool's 1 is set or the read fails. install.sh skips the check in those two cases and says so. Covered by testRestoreCheckFailsWhenListingPassesButRunningNeedsAPassword, testStartIsRefusedWhenListingPassesButTheRestoreNeedsAPassword, testRestoreCheckIgnoresACachedCredential, testStartIsRefusedWhenOnlyACachedCredentialWouldRunTheRestore, testStartWhileSleepIsAlreadyOffRunsNothing, testStartRunsTheRestoreTheJournalOwesBeforeThePrompt, testInstallStopsWhenOnlyTheCachedCredentialWouldRunTheRestore and testInstallRunsTheCheckOnlyWhileSleepReadsOn.

Greptile review of 12c8e40 (review 10, confidence 0/5). Both P0 findings are addressed in 23d625f. The earlier replies on those two threads, which deferred them, are superseded; no new thread reply was posted.

  • [P0] 4171743070 SleepGuard.swift:144: the restore preflight could SIGKILL sudo through CancellableCommand. Fixed by removing it: Start runs no sudo before the dialog. The privileged commands at Start are osascript (SIGTERM only; .stillRunning keeps the recovery lock) and, inside it, the root command's sudo and pmset, which the app never signals. The installer's sudo calls go through Backstop: run only the copy sealed in the signed bundle #28's supervisor, SIGTERM only, with fd 9 kept. Covered by testStartRunsNoSudoBeforeTheDialog, the OsascriptAdministratorPromptTests SIGTERM and still-running tests, testTheMarkerCannotBeRemovedWhileTheRestoreCheckRuns and the installer's ignore-SIGTERM tests.
  • [P0] 4171743074 SleepGuard.swift:139: the read-then-restore could change a setting no journal entry covered, and install.sh had the same window. Addressed in both. install.sh runs no pmset. The app's run of the restore moved into the root command, after the journal entry and the armed backstop. Not closed: pmset has no compare-and-set, so a 1 another tool sets after Start's read is still set to 0, at the end or for a moment by the check. The docs and Not covered state that residual for the maintainer to accept or reject. Covered by the RootCommandTests above and testStartWhoseRestoreCheckFailsRollsBackWithNothingToUndo.

Greptile review 11 of fa281c1 (confidence 2/5):

  • [P1] 4211874293 AdministratorPrompt.swift:227: a restore check that ends at or after the deadline still ran disablesleep 1. Fixed in bf4d6e2 (rootCommand) and 2f6b11f (the AppleScript copy, which bf4d6e2 missed). Covered by testDoesNothingWhenTheRestoreCheckEndsAtOrAfterTheDeadline (fake clock at the deadline, 1 s and a day past: exit 4, only the restore ran), the control testTurnsSleepOffWhenTheRestoreCheckEndsBeforeTheDeadline, testAFailedRestoreCheckStillExitsFiveOnTheFakeClock and testTheAppleScriptEmbedsTheRootCommandUnchanged. All of them run the command read back from the AppleScript. The mutation spot checks under New coverage fail them with the recheck taken out.
  • [P1] outside the diff (issue comment 6046657261), install.sh:614 at fa281c1: an upgrade from a bundle that does not declare InsomniaResumeFrozenVersion, with a frozen process in the journal, stopped at the recovery, because the staged backstop resumed through the installed binary. Main has the same stop (install.sh:635-636 at 781b596); on this branch it also left the three-line rule ahead of the older app. Fixed in 2b8028c: install.sh runs the staged copy with --own-bundle, so the staged binary, which declares the interface, resumes the processes under the recovery lock before the swap, and the old app never runs. Entries that binary cannot settle stay with their startedAtMicros, and the install stops with the previous app and agent in place. Covered by the two upgrade tests, the five-case keep test and the two --own-bundle tests under New coverage; the control run without the flag fails both upgrade tests. No reply was posted on the comment.

Independent round 14 review (GPT-6.1-Sol, xhigh) of 2b8028c. Verdict: needs changes, one P1.

  • [P1] AdministratorPrompt.swift:234 and the literal at line 244: the restore proof cleared another tool's SleepDisabled 1 set while the dialog was up, also when the start then expired. Changed in 42e42e5 (What, Decisions): the root command reads the setting as root after the dialog and stops on a 1, and the proof undoes the command's own change. The reproduction now exits 6 after one pmset -g, with no sudo call and the foreign 1 kept, whether the deadline passes during the check or not (testASettingMadeWhileTheDialogWasUpSurvivesADeadlineDuringTheCheck). Through Start, the rollback then runs no pmset (testASettingMadeWhileTheDialogIsUpIsLeftAlone and testASettingMadeWhileTheDialogIsUpSurvivesALateEnd end to end, and testASleepSettingMadeWhileThePasswordIsTypedIsLeftAlone). Not closed: Not covered states the remaining window and the decision it needs.

Independent round 17 review (GPT-6.1-Sol, xhigh) of 42e42e5. Verdict: needs changes, three blockers. All three are changed in 576c215 (What, Decisions); the Greptile thread 4213796939 on R1 is left open for review.

  • [P0] R1, AdministratorPrompt.swift:292 and the literal at line 303 (Greptile 4213796939): root ran disablesleep 1 before the user's proof, so with the rule missing an interrupted command, or a failed root fallback, left sleep off with no unattended restore. Changed: the command writes nothing until sudo's -V, -k -n -l and -k -n -ll answers fit the rule, and its only write is the last step. Covered by testEveryOtherPolicyRefusesBeforeAnyPmset, testARefusedRestoreQueryLeavesSleepOnWithNoUndo and the R1 harness rows. Not covered states what a listing cannot show.
  • [P1] R2, the same literals and SessionManager.swift:1066: the proof and root's fallback set 0 over another tool's 1, and with the dialog's output gone a refusal died by SIGPIPE (lockf 70), which the start undid. Changed: no write before the answers, and SIGPIPE is ignored, so every refusal keeps its status and leaves the 1. Covered by testARefusalKeepsItsStatusWhenTheDialogsOutputIsGone, testLeavesASleepSettingMadeWhileSudoIsAsked, testASudoRefusalLeavesASettingMadeWhileSudoIsAsked, testASettingMadeWhileSudoIsAskedSurvivesARefusal and the eleven-row table. Narrowed, not closed: Not covered states the read-to-write moment and the ambiguous exits.
  • [P1] R3: a read could use up the deadline, and the write still followed at or past it. Changed: the clock is compared after the queries and right before the write, and equality refuses. Covered by testWritesNothingWhenTheDeadlineComesDuringAnyCallBeforeTheWrite, testAJournalOwnedSettingWritesNothingWhenTheDeadlineComesDuringTheQuestions and testAnEndDuringRootsReadWritesNothing.

Independent round 19 review (GPT-6.1-Sol, xhigh) of 7dcf51f. Verdict: needs changes, three P1s. Changed in c5456f8 (What, Decisions); none is waived.

  • [P1] F1, AdministratorPrompt.swift:314 and its literal: sudo -V does not list an approval plugin with no show_version, which still rejects the restore when it runs. Changed: any /etc/sudo.conf, the only file that loads plugins, refuses before any sudo call, and so does a PAM session stack other than macOS's own. Covered by testAnySudoConfStopsTheCommandBeforeSudoRuns, testTheSilentApprovalFixtureChangesNothingButTheRestore, testOnlyMacOSsOwnPamSessionLinePasses and testASudoConfStopsTheStartBeforeSudoIsAsked.
  • [P1] F2, the same literals: user and global Defaults that make the restore fail passed the listing. Changed: only listed Defaults pass, and only sudo 1.9.17p2. Covered by testTheListingReaderTakesOnlyTheDefaultsItAccepts, testTheVersionReaderTakesOnlySudo1_9_17p2WithTheSudoersPlugins and testUserDefaultsTheCheckDoesNotAcceptStopTheStart.
  • [P1] F3, the root read and write and SessionManager.swift:1041 and :1087: the ownership requirement stays unmet. Narrowed: a failure whose marker still holds the bare nonce runs no undo. Not closed; Not covered lists what remains and the alternatives. Covered by the marker tests listed under New coverage. Round 22 replaces the marker rule with the receipt (below).

Independent round 21 review (GPT-6.1-Sol, xhigh) of c5456f8. Verdict: needs changes, two P1s and one P2. Changed in bd7db43 (What, Decisions); none is waived.

  • [P1] Finding 1, Store.swift:344 (RemovedMarker.isUntouched), read at SessionManager.swift:1097 and :2601, with the record written at AdministratorPrompt.swift:369: root wrote the record into the marker as the user, so a process running as the user could put the bare nonce back into the same inode after pmset's write. A start that then failed was rolled back with no undo, and sleep stayed off with no journal entry. Changed: the record is in a receipt only root can write, under folders only root can change, and the marker is no longer evidence. isUntouched is gone. Covered by testAForgedMarkerDoesNotDropTheRestore and its matched control testAFailureAfterTheWriteIsUndone, the receipt trust tests in SleepOffReceiptsTests and testTheShippedCommandTrustsOnlyRootsReceipt.
  • [P1] Finding 2, SessionManager.swift:528, :1097, :1495 and :2149: relaunch, backstop.sh and uninstall.sh deleted the marker without reading the record, so an abandoned dialog's session was resumed on another tool's 1 while it was valid, or undone as Insomnia's own once it had expired. Changed: the start journals sleepOffAttempt, and every reader that deletes the marker settles the start from the receipt: its session.json never resumes, and the sleep entry goes back to what was owed before unless the receipt shows this start's writing or cannot be trusted. Narrowed, not closed: the after-record phases and the read-to-write moment stay (Not covered). Covered by the relaunch tests in SleepOffSettlementTests (unexpired, expired, an earlier owed restore, writing, evidence that does not match, no dialog, a marker with no journaled start, a settlement that cannot be written) and the backstop.sh, uninstall.sh and install.sh tests listed under New coverage.
  • [P2] Finding 3, README lines 218, 221, 226, 233, 235 and 398, and SECURITY.md line 134, with Greptile 4215430637 on README line 221: the README promised that a 1 set while the dialog is up always survives, that a missing record proves no write, that only a command already turning sleep off is waited for, and recovery within a minute; it described the 120 s limit as covering the command, and SECURITY.md called a hung query bounded by it. Changed: README and SECURITY.md now separate a cancelled dialog, a wrong or unanswered password and a command that times out after the password; say that a timeout or a lost answer is settled from the receipt and can still clear another tool's 1; say what holds the marker's lock and that launchd does not promise when the agent runs; give no time limit for the queries; and name the requesting user's Defaults the list accepts apart from the Runas and command-bound section it refuses. No reply was posted to the Greptile thread.
  • F3, the ownership requirement from rounds 14, 17 and 19, stays open and unwaived. Not covered lists what remains, which phases come from this PR and which from main, and the alternatives.
  • The round 20 entry above names two hand mutations of the marker rule. As the review notes, their saved diffs hold only 12-byte closing fragments, so those results cannot be checked from the evidence and are not certified. They were not rerun; the rule they mutated is gone.

Independent round 23 review (GPT-6.1-Sol, xhigh) of bd7db43. Verdict: needs changes, seven P1s and one P2. Findings 1 to 6 and 8 are changed in 7e3ddc8 and e741a7f (What); finding 7 stays open and unwaived. Green checks and resolved threads were not taken as clearance.

  • [P1] Finding 1, SleepOffReceipts.swift:104, with Greptile 4217474520: a valid receipt holding another nonce read as "never wrote", and every folder of the user shares the receipt, so a start in a second folder overwrote the first start's writing. Changed: the claim in <uid>.released, the predecessor in the line, and exit 8 when the receipt no longer begins with it. Another start's line naming the same predecessor shows the first start never wrote; one naming another predecessor shows it may have.
  • [P1] Finding 2: a reader that deleted a replaced marker finished recovery while the original command could still write. Changed: every reader takes the receipt's lock, which the command holds through pmset, and a busy or failed lock decides nothing.
  • [P1] Finding 3: a recreated settled marker let an old dialog write. Changed: expires is at most 130 s after the marker. Until then, a receipt that still holds the predecessor decides nothing unless the dialog ended by itself. A command that comes later stops at expires (4), and one for a settled start whose line moved on stops at the predecessor (8).
  • [P1] Finding 4: a never-write settlement that could not be published restored sleep. Changed: it keeps the attempt and runs no pmset unless an earlier restore is owed.
  • [P1] Finding 5: dd conv=notrunc,fsync is fsync, not F_FULLFSYNC. Changed: perl's fcntl F_FULLFSYNC (51) with a refusal before pmset when perl or the flush fails. Sources: Apple's fcntl(2) and fsync(2) pages, xnu's fcntl.h, perlfunc. Untested: what a drive does with the request, which Macs ship /usr/bin/perl, and a real power cut.
  • [P1] Finding 6, with Greptile 4217474536: settlement cat and head through PATH. Changed: CAT=/bin/cat and HEAD=/usr/bin/head in the fixed-tool blocks, covered by the fake substitution map and by shadowed-PATH tests for backstop.sh and uninstall.sh. The bot's P0 root-privilege claim was not established; the conditional P1 recovery defect was.
  • [P1] Finding 7, ownership: open and unwaived. Not covered separates what this PR creates (a recorded write not known to have happened, the gap between the read and the write) from what main already does (a confirmed session or an earlier owed restore), and lists the alternatives and their costs. Tests that show a foreign 1 being cleared describe the finding; they are not guards.
  • [P2] Finding 8, README around lines 232, 259 and 266, SleepOffReceipts.swift:34, SessionManager.swift:2660 and the marker rule in config.json: another nonce does not prove no write and a deleted marker does not stop an old dialog. Changed with the code: every description now gives the three never-wrote receipts, when "may have" applies at once, when only after expires, and that undecided keeps the start recorded.
  • Prior findings and threads: the round 23 review's table (original preflight threads, R1 to R3, the upgrade threads, Independent19 F1 and F2, Independent21 F1 to F3) stays as it disposed them; round 24 removes none of those corrections.

Independent round 25 review (GPT-6.1-Sol, xhigh) of e741a7f. Verdict: needs changes, four P1s (one of them F7) and two P2s, plus the hosted Swift job's watchdog failure. Findings 1 to 4 and 6 are changed in 93fb3dc (What); finding 5, F7, stays open and unwaived. Green checks and resolved threads were not taken as clearance.

  • [P1] Finding 1 (R25-1): a settlement gave the claim back before it published the journal, so a crash between the two, then later starts of another folder, could lose a never-wrote result and clear another tool's 1. Changed: every settlement (app, backstop.sh, uninstall.sh, and install.sh through the staged backstop.sh) journals its decision as sleepOffAttempt.settled while the claim is held, then gives the claim back and drops the record; a settled record is only finished, never read against the receipt again. Backstop version 5.
  • [P1] Finding 2 (R25-2): an undecided start allowed the sleep undo once expires had passed or an earlier restore was owed, while the command may still be in pmset. Changed: undecided holds the sleep undo whatever the time and whatever is owed; the owed restore stays journaled and runs after the lock is let go. The cost is in Not covered: a command that never exits, or any account holding the lock, delays recovery indefinitely.
  • [P1] Finding 3 (R25-3), with Greptile 4220347392: install.sh read the release file before the receipt's lock and freed a held claim after making the receipt. Changed: the lock comes first, every decision reads the receipt and the release file under it, a held claim is kept (also right after the receipt is made) or stops the install when its file is unsafe, and a repair checks the receipt, the folders and the release file's metadata and bytes again just before sudo install replaces it.
  • [P2] Finding 4 (R25-4), the outside-diff comment 6046657261: prepare_low_power_off, as Display: refuse the private brightness calls on an unmeasured macOS or a changed KeyboardBrightnessClient #43 merged it, ran head through PATH. Changed: both calls use "$HEAD"; a hostile head first on PATH no longer stops the boot record or Low Power Mode off. Other inherited bare calls were left as main has them. The bot's root-execution impact was not established; the conditional recovery refusal was. On 93fb3dc Greptile changed 6046657261 to "No findings outside the diff remain." (2026-10-08T17:58:42Z), and Greptile's thread 4220347392 shows as resolved; neither is an independent review.
  • [P1] Finding 5 (R25-5, F7), ownership: open and unwaived. The root command now reads pmset -g again after the flushed writing and before its last clock check and pmset, and refuses with refused and exit 6 on a 1 or a failed read. That narrows the A and B windows and is not an owner token. Not covered lists the remaining histories, the alternatives and their costs.
  • [P2] Finding 6 (R25-6): docs claimed more than the protocol proves. Changed: README, SECURITY.md, spec, release-validation.md, both Greptile rule sources and the source comments now give the settlement order, that removing the marker does not stop a command already running, that a busy lock can outlast the answer window, the wall-clock assumption behind "never wrote" from expires, the retry after a failed publication or claim release, and the availability costs.
  • CI: the hosted job exceeded its 1200 s watchdog with no failed case. Fixture setup was measured and is not where the time goes (Tests); one new test was made 9.6 s shorter. No rerun, timeout change or matrix cut. The job remains at risk.
  • Prior findings: independent23 F1 to F6 and F8 keep their round 24 corrections, now with this round's changes on top (F1 and F4 through finding 1, F2 through finding 2, F3 with the stated wall-clock limit, F6 through finding 4); F5's flush is unchanged and still unmeasured on real drives; F7 stays open. The earlier table (original preflight threads, R1 to R3, the upgrade threads, Independent19 F1 and F2, Independent21 F1 to F3) stays as the round 23 and 25 reviews disposed them.

Independent round 27 review (GPT-6.1-Sol, xhigh) of 93fb3dc. Verdict: needs changes, five P1s (one of them F7) and four P2s, the hosted Swift job's watchdog failure among them. Findings 2 to 8 are changed in d933b68 (What); finding 1, F7, stays open and unwaived; finding 9 has fixture changes and no CI change. Green checks and resolved threads were not taken as clearance.

  • [P1] Finding 1 (R27-1, F7), ownership: open and unwaived. Narrowed for the app that received exit 6 and could not journal its rollback: its later settlements of that start keep "never wrote". A2, the rest of A3, B2 and C2 still clear another tool's 1. Not covered lists the histories, what was considered and the alternatives with their costs.
  • [P1] Finding 2 (R27-2): uninstall.sh removed the shared rule before it found another folder's claim. Changed: the receipt's lock and the shared check come before any removal and refuse the whole uninstall; the lock is held, and the files checked again, through the shared removals.
  • [P1] Finding 3 (R27-3): any account could lock the 0644 receipt. Changed: mode 0600 and one user:<name> allow read entry, made and repaired by install.sh and required by every reader. The installing user can still hold the lock, and a command that never exits still holds it; both costs stay (Not covered).
  • [P2] Finding 4 (R27-4): a settled successful start ended its healthy session on relaunch. Changed: that start's own session resumes, with Starts refused until the cleanup finishes.
  • [P2] Finding 5 (R27-5): recovery reads ran tools through PATH. Changed at every listed site, the diagnostic excerpt and the status-PID reads; the bare calls left are listed in Not covered.
  • [P2] Finding 6 (R27-6): source messages overstated revocation, write evidence and the answer window. Changed in the app, both scripts, the root command's docs, README, SECURITY.md, spec, release-validation.md and both Greptile rule sources.
  • [P1] Finding 7 (R27-7): the generic supervisor lost fd 9 under a group TERM. Changed: TERM and HUP ignored and errexit off for the call's life, the defaults given back to the call only. Matched controls on the old function and on a mutant without the call's reset fail as expected.
  • [P1] Finding 8 (R27-8): uninstall.sh counted failed journal reads as clean. Changed: a private copy, every read's failure carried to the decision, a stop before any removal, and state.json removed only while it equals the checked copy. The new tests fail 8 of 9 on the 93fb3dc scripts.
  • [P2] Finding 9 (R27-9): the hosted job hit its 1200 s watchdog. Fixture changes only: AppEncodedJournalScriptTests runs its script rows two at a time, which saved 24 s of 132 s against a same-hour serial control and does not close the gap, and the fake sudo's wait loop runs no command substitution after its trap. No workflow, watchdog, partition or rerun change; the two-job split is the parent's choice. Its hosted result at d933b68 is in Not covered (CI).
  • Prior findings: R25-1 to R25-4 and R25-6 keep their round 26 corrections, with this round's changes on top (R25-2's undecided hold now has a smaller cost through finding 3; R25-6 through finding 6). independent23 F1 to F6 and F8 keep their round 24 corrections; F5's flush is still unmeasured on real drives. The earlier table (original preflight threads, R1 to R3, the upgrade threads, Independent19 F1 and F2, Independent21 F1 to F3) stays as the round 23, 25 and 27 reviews disposed them.

Independent round 29 review (GPT-6.1-Sol, xhigh) of d933b68: needs changes, seven P1s and six P2s. Changed in 468db03, 1468950, 7e233e8, 29949a6 and f643d49 (What): findings 2 and 4 to 12, and 13's capture errors. Open: 1 (F7, narrowed, unwaived), 3 (reported only) and 13's runtime (Not covered). Green checks and resolved threads were not taken as clearance.

  • Prior findings keep their corrections; this round closes the gaps the review named in R25-2, R25-3, R25-4 and 4162335989.
  • Greptile P1s 4228725521 and 4228725533 on 7e233e8: changed in 29949a6 (What); threads not resolved here.

🤖 Generated with Claude Code

RetriggerConfidence Score: 4/5

Fix the uninstall rerun’s race with receipt creation before merging.

Fix All in Claude CodeFindings

  1. P1 Rerun deletes new install files ▶
Fix with agent prompt
### Issue 1
scripts/uninstall.sh:2108-2111
The new `finishing` path proceeds without holding the receipt’s lock. If this rerun uses another `INSOMNIA_HOME`, a standard installer has a different recovery lock and can recreate the shared receipt after the rerun’s last `shared_unchanged()` check. The removal loop then deletes the new receipt and `.released` by path, even while the installer holds the new receipt’s lock. That installation loses files required for Start and recovery.

Serialize this cleanup with receipt creation using a shared lock that survives receipt removal.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

This PR removes passwordless sleep disabling and asks for an administrator password at Start. The latest changes improve recovery when files cannot be created and let an interrupted uninstall finish removing its receipt pair.

  • Starting a session asks for a password before sleep turns off.
  • Recovery settles interrupted starts from a shared receipt.
  • The installer leaves sleep-on commands passwordless and asks for access when starting.

Diagram

sequenceDiagram
  participant U as Uninstall rerun in another home
  participant I as Standard installer
  participant F as Shared receipt files
  U->>F: Check receipt absent and recorded release unchanged
  Note over U: No shared receipt lock held
  I->>F: Create and lock new receipt
  U->>F: Remove receipt and release by path
  Note over I,F: New installation loses required files
Loading

Reviews (21) · Last reviewed commit: "Round 30: two fixtures follow the reader..." · Reviewed by Greptile

…trator password

After install, any process running as the user could run
`sudo -n pmset -a disablesleep 1` with no password and with Insomnia
not running. Sleep turned off that way is not journaled, so the
recovery agent never undoes it, and the grant lasts until uninstall.

install.sh now writes three lines instead of four: disablesleep 0,
lowpowermode 1 and lowpowermode 0. None of them can keep the Mac
awake; turning sleep back on and the Low Power Mode floor stay
passwordless so the app, backstop.sh and uninstall.sh can recover
unattended. The file is always rewritten, so a reinstall over the old
four-line rule drops the disablesleep 1 line. The installer's
`sudo -n -l ... disablesleep 0` check is unchanged.

The app turns sleep off through the standard macOS administrator
dialog: /usr/bin/osascript running one fixed literal,
`do shell script "/usr/bin/pmset -a disablesleep 1" with administrator
privileges with prompt "..."` (AdministratorPrompt.swift). A child
process rather than NSAppleScript in-process, because AppleScript is
main-thread only and a dialog waited on from the main actor would
freeze the menu bar and the lifecycle queue with no way to time out.
120 s limit; at the deadline osascript gets SIGTERM and nothing
stronger, and the runner waits for it (and the pipe its root pmset
inherits) to finish before reporting the timeout, so nothing is rolled
back while a root pmset may still run. The runner is injected behind
AdministratorPromptRunning; PmsetSleepGuard routes only
setSleepDisabled(true) through it.

Start order is unchanged (session.json, journal, backstop armed, then
the dialog), so a crash mid-prompt leaves recovery a record. Cancel,
wrong password, timeout and pmset failure all take the existing
startFailed path; the notification now names the prompt.

Reconcile no longer re-applies the guard for a valid session (that
would prompt at login or after a crash with nobody at the keyboard).
It reads pmset -g: SleepDisabled 1 continues the session as before,
without a prompt; 0 ends it with a new reason, sleepReenabled, and the
notification "Sleep was turned back on while Insomnia was not running,
so the session ended." A failed read ends it too. Start is the only
caller that can prompt; there is no auto-start, URL scheme or
scheduled start.

Tests: AdministratorPromptTests (the osascript runner against a fake
osascript: exact script literal, cancel, failure, launch failure,
SIGTERM-only timeout that waits for the child; PmsetSleepGuard
delegation; lifecycle: start, cancel/fail/hang roll back clean with the
record present while the dialog is up, reconcile with sleep still off
continues without a prompt, with it on ends without a prompt, only
Start prompts), RecoveryScriptTests (install writes exactly the three
lines; reinstall over a four-line rule leaves three), and the existing
reconcile tests updated. README, SECURITY.md, spec sections 1, 2, 8, 9
and the manual plan, and five "Not run" rows in the validation record.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread Sources/Insomnia/System/AdministratorPrompt.swift Outdated
Comment thread scripts/install.sh Outdated
…d sudoers line back when the app will not quit

Two review findings on PR #32.

The osascript runner read both pipes to EOF and only then looked at
whether the deadline had fired. An osascript that did not exit on
SIGTERM, or a root command holding its output, would keep the start
transaction, and with it the lifecycle queue and the recovery lock,
waiting forever, with no message and no way out short of killing the
app. The runner now drains the pipes on their own threads and, 3 s after
the deadline (the backstop's grace), answers a caller still waiting with
AdministratorPromptError.stillRunning: osascript's pid, whether osascript
itself is still alive, and a handle that resolves when the child has
been reaped and its output has closed. Nothing is killed. The start
keeps session.json, the journal entry and the recovery lock, because the
command may still turn sleep off and a backstop running beside it would
clear a journal the late pmset then contradicts. It posts "Password
prompt still running" with the pid, offers kill <pid> only while the pid
is osascript's own, puts the same on the menu warning line, waits for
the handle, and then runs the usual startFailed rollback. That is the
rule PR #22 applies to a stuck sudo pmset; the refusal path it adds can
take stillRunning once both are in.

install.sh wrote the three-line rule and then asked the running app to
quit. If the app did not quit within 15 s, the previous bundle stayed
installed with a rule that denies the sudo -n disablesleep 1 an older
build starts sessions with, so that install could not start a session
until a rerun. The rule is still written first: writing it after the
bundle would let a cancelled password prompt on a fresh install leave a
new app with no undo rule. On that one stop the installer now writes
the rule again with the disablesleep 1 line, using sudo's cached
credential, and says which rule is in place, including when the second
write fails. A successful install writes the file once. The rule is
printed by one function so the two writes cannot drift.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Comment thread scripts/install.sh Outdated
Comment thread Sources/Insomnia/Core/SessionManager.swift Outdated
…rdless-sleep-off

# Conflicts:
#	SECURITY.md
#	docs/spec.md
Comment thread scripts/install.sh Outdated
…ep 1

Review finding on PR #32: the previous fix put the passwordless
`pmset -a disablesleep 1` line back when an upgrade stopped because the
running app would not quit. That re-grants every process running as the
user a way to keep the Mac awake with no journal entry, until a rerun or
uninstall, which is the exposure this PR exists to remove.

The installer now never writes that line. It asks a running Insomnia to
quit first and stops with nothing changed, the sudoers file included, if
the app is still running after 15 s. Then it writes the three-line rule,
checks that the app was not opened again during the password prompt, and
replaces the bundle. A stop between the rule and the new bundle (rule not
effective, app opened again, a failed copy or signature) leaves an older
build unable to start a session; an EXIT trap armed for exactly that
window prints a plain note with the rerun command. That fails closed.

If the password prompt fails after the app was quit, the message says the
app was quit and nothing else changed, so the old build can be opened
again with its own rule.

Four installer tests encoded the old password-before-quit order and are
rewritten for the new one. A static test checks that no non-comment line
of install.sh mentions `disablesleep 1`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread scripts/install.sh
krishhgg and others added 3 commits October 2, 2026 00:23
A cancelled or failed sudo password used to arrive after the installer
had already quit Insomnia, which ends the session, so a failed upgrade
cost the user their running session for nothing.

install.sh now runs `sudo -v` before anything else. A cancelled or
failed password exits with nothing changed and the app still running.
Then it quits the app (stopping with nothing changed if it is still
running after 15 s), writes the three-line rule on sudo's cached
credential (asking once more only if `sudo -n -v` says it expired
during the quit), checks the app was not reopened, and replaces the
bundle. When session.json holds a future deadline it first prints
"A session is running and the upgrade will end it." and, with a
terminal on stdin, asks "Continue? [y/N]"; anything but y stops before
any sudo call. No path writes `disablesleep 1`.

The stuck-prompt notification no longer says `kill <pid>`: it stays in
Notification Center after osascript exits, when the pid may belong to
another process. The hint stays on the menu warning line, which is
replaced once osascript exits.

.greptile/config.json and rules.md described the four-line grant this
branch removes. The rule is now sudoers-rule-is-three-exact-lines, and
the instructions and the journal, backstop and SIGKILL rules name the
administrator prompt as the only path to `disablesleep 1`.

Tests: fixture scripts get /dev/null as stdin, or a pty holding the
answer. New installer cases cover a cancelled password during a
session, n and y at the question, an expired session, an expired
credential and a failed second password; the existing ones now expect
`sudo -v` first. README, SECURITY.md, spec section 2 and two new
release-validation rows follow the new order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A password dialog can outlive its start. If the app crashed or was
force-quit with the dialog up, osascript kept running; reconcile or the
backstop then cleared session.json and the journal, and a later answer
still ran `pmset -a disablesleep 1` with nothing left to undo it.

Each Start now writes a random nonce to APP_SUPPORT/pending-start just
before the dialog. The dialog runs a fixed root command, /bin/sh -c
AdministratorPrompt.rootCommand with the marker path and nonce as $1
and $2 (passed through `quoted form of`, compared, never run). It runs
pmset only while the marker holds the nonce, reads it again after
pmset, and turns sleep back on if it is gone by then (exit 3 and 4).
The start deletes the marker on every outcome before it releases the
recovery lock, and at once on a stuck prompt. Every other lock holder
deletes it before it touches the journal: exclusive() in the app,
backstop.sh right after lockf, and uninstall.sh before it runs a
possibly older backstop; uninstall refuses to remove anything while
the marker is present. A marker that cannot be written rolls the start
back with no dialog. SleepGuarding.setSleepDisabled(Bool) is split into
disableSleep(PendingStart) and enableSleep(), so no path can turn sleep
off without a marker. The sudoers rule keeps its three lines.

The stuck-prompt menu line kept offering `kill <pid>` after osascript
had exited while a command it started still held its output. The
runner now reports osascript's own exit to the handle as soon as it is
reaped (UnfinishedPrompt.waitUntilOsascriptExits), and the start
replaces the line then, before it waits for the output to close and
rolls back.

The runner tests started the deadline while the fake could still be
installing its TERM trap, and the output-holder test relied on a 5 s
sleep. OsascriptAdministratorPrompt takes a beforeDeadline hook; tests
block in it until the fake has written a ready file after its trap,
and every holder runs until the test releases it (60 s watchdog).

Tests run the real root command under /bin/sh with AppleScript's
quoting and a fake pmset: nonce match, missing marker, a newer start's
nonce, an empty nonce, the marker deleted while pmset runs, and a path
and nonce full of quotes and $(...). Lifecycle tests cover the marker
around the dialog, fresh nonces, a marker that cannot be written, the
relaunch path and a newer start after it; script tests cover the
backstop path, lock contention and uninstall. osacompile checks the
script compiles. Docs, spec sections 1 and 8, a release-validation row
and the Greptile rules describe the marker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread scripts/backstop.sh Outdated
Comment thread Sources/Insomnia/System/AdministratorPrompt.swift Outdated
Comment thread Sources/Insomnia/System/AdministratorPrompt.swift Outdated
Comment thread scripts/backstop.sh Outdated
Review of a2ffdd4 found four ways the marker could still fail. The
root command re-read the marker after pmset and ran `disablesleep 0`
if it was gone, but ignored that pmset's status, and an old command
whose marker had been replaced by a newer start's turned sleep back on
during the newer session. Recovery that could not delete the marker
(an immutable flag, an ACL) logged it and cleared the journal anyway,
leaving an old dialog able to turn sleep off with nothing to undo it.
A cancelled dialog took the undo path and ran `disablesleep 0`, which
cleared a SleepDisabled another tool had set.

The root command now runs as `/usr/bin/lockf -k -n -t 10 <marker>
/bin/sh -c <rootCommand>`. lockf holds the marker's flock from before
the nonce check until pmset exits and never creates the file (exit 69
when it is missing). Every deleter takes that lock first and then
unlinks: Store.removePendingStart (flock, polled up to 10 s, then
.markerBusy), and `lockf -k -n -s ... $RM -f` in backstop.sh and
uninstall.sh, both through a new RM=/bin/rm. So the marker goes either
before the check, which fails, or after pmset, which the journal entry
still covers. The second read and the compensating pmset are gone, so
neither can fail or act on a newer start.

sleepDisabledByUs is now cleared only by a transaction that removed
the marker. A marker that cannot be locked or deleted leaves recovery
incomplete: sleep is still restored, the entry stays, and the app
reports it (log, "Restore incomplete", menu line), refuses new starts,
and retries on every transaction. backstop.sh keeps the entry, adds the
marker to its failures and exits 1, also with a clean journal.

A cancel (osascript's stderr ending in "(-128)") and a launch failure
ran nothing as root, so the start restores session.json and the journal
exactly and runs no pmset. A wrong password, a timeout and a pmset
failure keep the undo path. A stuck prompt whose command still holds
the marker lock gets the marker removed once the prompt exits.

The SIGTERM test now sets its stop grace and accepts .stillRunning,
waiting on its handle, so a slow TERM handler cannot fail it.

Tests run the real root command under lockf with a fake pmset (marker
missing, removal blocked while pmset runs, an answer that waits on a
removal), Store removal (held lock, uchg, directory, dangling link),
the app's keep-the-entry and refusal paths, cancel and launch failure
with a foreign SleepDisabled, and the backstop and uninstall with a
locked or undeletable marker. Docs, spec sections 1, 8 and 9, two
release-validation rows and the Greptile rules describe the lock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread Sources/Insomnia/Core/SessionManager.swift Outdated
Comment thread scripts/install.sh Outdated
krishhgg and others added 8 commits October 2, 2026 15:45
Round three of review on the pending-start marker.

- Start reads the installed backstop.sh's "# insomnia-backstop-version:"
  line and shows no dialog below 2, the first version that deletes
  pending-start, telling the user to run install.sh again. install.sh
  now installs backstop.sh under the recovery lock before the bundle,
  with install -S so a running older copy keeps its inode, and waits up
  to 30 s for runs of the older script to exit; it stops before the
  bundle if one stays or pgrep fails.
- A stuck prompt whose marker the start deleted under the marker's lock
  can no longer run pmset, so the start rolls back and releases the
  recovery lock at once. A task outside the transaction keeps the menu
  line true: the kill hint goes when osascript exits, the line once the
  prompt has. A prompt whose command holds the marker's lock is still
  waited for, as before.
- The root command gets the session's end as $3 and refuses (exit 4)
  unless /bin/date +%s is below it. A $3 that [ cannot compare refuses
  too. The text stays one fixed literal.
- A journal write that fails after sleep was restored is logged and
  reported like the other failed clears instead of being dropped with
  try?; the entry stays and the next run retries.
- install.sh calls rm, rmdir, mkdir, cp and install through fixed
  paths, the EXIT traps included.
- The marker-removal test waits until lockf is blocked on the marker's
  lock (proc_pidinfo) instead of sleeping 300 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since this branch, reconcile reads `pmset -g` and ends a session whose
sleep is back on. Three App Nap reconcile tests and
testDeadlineTimerFiresEnd came from main without that seed, so they
saw sleep on and ended the session early. They now start with
sleepDisabled set, and testDeadlineTimerFiresEnd expects the pmset
read before the undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install.sh still made and wrote the temporary sudoers file with bare
mktemp and cat, and handed sudo bare `visudo` and `install`. sudo looks
a bare name up in the caller's PATH and runs what it finds as root, so
a tool first on PATH could change the rule that lands in
/etc/sudoers.d. The LaunchAgent move used a bare mv.

The tool block now has MV, MKTEMP, CAT and VISUDO, and sudo is given
"$VISUDO" and "$INSTALL". The fake sudo only knows visudo and install
by those full paths. testInstallCleansUpWithoutPATH also puts mv,
mktemp and a cat that adds `NOPASSWD: ALL` to any rule first on PATH,
and checks the installed rule never gets that line. The fixed-path rule
in .greptile/config.json names the new tools and the sudo case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
uninstall.sh asked sudo to run bare `test` and `rm` on the sudoers
rule, so sudo looked them up in the caller's PATH and ran what it found
as root. They are now "$TEST" (TEST=/bin/test) and "$RM".

The fake sudo only knows /bin/test and /bin/rm. With sudo-root.mode
"search" it can see through a directory the user cannot search, as
root can. testUninstallFindsAndRemovesARuleOnlyRootCanSee uses that to
cover the `sudo test -e` branch, which no test reached before because
the fixture's rule was always visible to the user.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
testEndDuringReconcileMustNotLeaveSleepDisabled came from main (#44).
It parks reconcile's `disablesleep 1` on sleepGate and waits for that
call to start. Reconcile on this branch never turns sleep off again: it
reads `pmset -g` and keeps the session only if sleep is still off. The
gate never opened and the async test hung, which stalled both full runs
after the merge.

The test now seeds sleep off and parks reconcile on readGate, the
`pmset -g` read. It still checks that an end requested meanwhile queues
behind reconcile and that `disablesleep 0` is the last call. An end that
skips the queue fails it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The osascript runner this branch adds waited with
`process.waitUntilExit()` on a GCD worker and no terminationHandler.
#46 found that this can wait forever on macOS 26 for a child that has
already exited. A Start whose prompt never reports its exit keeps the
recovery lock. The runner now creates a ProcessExit before `run()` and
waits on it, as Shell, CancellableCommand and ShellTimeout do since
#46.

The test helpers this branch adds follow: RootCommandProcess and the
osacompile check wait on a ProcessExit, and the backstop lock test
stops its holder with `stop()`. #46 changed holdLock() to return a
LockHolder, so that test did not compile on top of main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread Sources/Insomnia/Store/Store.swift Outdated
Comment thread scripts/install.sh Outdated
Comment thread scripts/install.sh Outdated
Comment thread scripts/backstop.sh
krishhgg and others added 6 commits October 2, 2026 18:59
Conflicts, each resolved by keeping both sides: the notification titles
in SessionManager, the StoreError cases, the test helpers in
TestSupport, the tool block in uninstall.sh (one RM line), and spec
step 1/2 (main's unreadable session.json text, then this branch's
step 2). backstop.sh had RM twice after the merge; main's line stays.
TestIsolationTests now also checks that pending-start resolves inside
the test home.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dialog turns sleep off on its own, but the end, backstop.sh and
uninstall.sh can turn it back on only through the passwordless
`sudo -n /usr/bin/pmset -a disablesleep 0`. With
/etc/sudoers.d/insomnia gone (an uninstall that stopped after
removing it, a hand-deleted file) a start would succeed and leave
sleep off past its deadline.

performStart now calls SleepGuarding.checkPasswordlessRestore after
the backstop version check and before it writes anything or shows the
dialog. PmsetSleepGuard runs `sudo -n -l` on the exact restore command
(PmsetSleepGuard.restoreArguments, which enableSleep also uses): it
lists, never runs pmset, and fails instead of prompting when a
password would be needed. A failure refuses the start with nothing
changed, a "Session not started" notification and a message that says
to run scripts/install.sh again.

Tests: the lifecycle tests cover the missing rule (no dialog, no
writes, the message) and the present one (checked once, before
session.json, the journal, the marker, the arm and the dialog).
PmsetSleepGuard takes a sudo path so a fake sudo can check the exact
argv and the failure text without running the real one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
lockf locks the file it opened, while the root command's nonce check
and every deleter go by path. A file put in the marker's place could
be locked and deleted while the root command still held the original
and ran pmset, and the start could then clear sleepDisabledByUs too
early.

Store.removePendingStart compares fstat of the locked descriptor with
stat of the path, and looks again if they differ. savePendingStart
returns the identity of the file it wrote, taken from the temp file
before the rename. When performStart voids a stuck prompt, it passes
that identity, so a replaced marker (a copy has no lock) or a missing
one (it went without the lock) does not count as voided and the start
waits for the prompt.

backstop.sh and uninstall.sh now open the marker on fd 8 (regular
files only, since open(2) on a FIFO blocks under the recovery lock),
lock that descriptor with "$LOCKF" -s -t N 8, and compare
"$STAT" -f %d:%i <&8 with "$STAT" -L -f %d:%i of the path before
"$RM" -f. The scripts never wrote the marker, so they cannot see a
copy swapped in before their open. The .greptile rule, rules.md, spec
and SECURITY.md say so. backstop.sh's inode helper now calls "$STAT"
too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run that could not delete pending-start recorded it and then exited 0
when session.json was still valid, or after it moved a malformed
session.json aside with nothing journaled. The marker stayed, but the
caller saw a clean run. Every exit 0 after the marker step now goes
through one check that logs the stuck marker and exits 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retire wait counted any process whose arguments contained the
installed backstop.sh path, so an editor or a tail on that file blocked
every upgrade. It now counts only a run: /bin/bash and the installed
path, plus --force, which is how launchd, install.sh and uninstall.sh
start it. The wait cannot use the recovery lock instead, because the
installer holds it and an old run is waiting on it.

The sudoers rule was written before the wait, so a timeout there left
the new rule beside the old app, which cannot start a session with it.
The rule is now written under the recovery lock after the new
backstop.sh is installed and the wait is over. Every stop before it
leaves the old rule beside the old app and says only backstop.sh
changed. The credential check moves to right after the quit, so a
failed second password still changes nothing.

The installer also looks for a reopened Insomnia after the wait, before
the rule, and once more right before it removes the bundle, so a
running old build is not left under a rule it cannot start sessions
with.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a start voiding its own stuck prompt knows which file it wrote. A
transaction after a relaunch has the same limit as backstop.sh and
uninstall.sh; SECURITY.md named only the scripts. The spec now states
the limit and why a same-user swap is not a new exposure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
krishhgg and others added 4 commits October 7, 2026 13:58
The restore check can be slow (sudo may wait on a directory service),
and the deadline was compared only before it, so a session that ended
during the check could still get `disablesleep 1` (Greptile 4211874293).
The command now compares /bin/date +%s with $3 again right before
`disablesleep 1` and exits 4 past it; the start is undone like an end,
and the only pmset that ran is the restore that undo runs anyway.

New RootCommandTests case holds the fake pmset in the restore check
until the clock passes the deadline. Docs: SECURITY, spec section 1,
.greptile/rules.md and the AdministratorPrompt comments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bf4d6e2 changed rootCommand but not the copy disableSleepScript embeds,
which is what osascript actually runs. The embedded copy is now
generated from rootCommand with AppleScript's escaping, so the two
match (testScriptIsTheExactLiteral, and a new test reading the command
back out of the AppleScript).

The wall-clock test from bf4d6e2 is replaced by deterministic ones.
RootCommandProcess takes the command to run and an optional fake clock:
/bin/date is replaced by a fake that reads a file, and the fake pmset
moves that file forward when the restore check runs it. The tests run
the command read back from the AppleScript, starting 100 s before the
deadline:
- the check ends 1 s before the deadline: restore, then disablesleep 1
- the check ends at the deadline, 1 s after or a day after: exit 4,
  only the restore ran
- the check fails: exit 5, no pmset

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FakeClock already names the app's injectable clock in TestSupport, so
2f6b11f's struct of the same name did not compile.

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

install.sh runs the staged copy's backstop.sh before the bundle swap. That
copy handed frozen entries recorded with startedAtMicros to the installed
binary, so an upgrade over a build whose Info.plist has no
InsomniaResumeFrozenVersion kept them and stopped after the three-line
rule was written (Greptile issue comment 6046657261).

backstop.sh --own-bundle takes the binary and Info.plist beside its own
copy. It finds them from BASH_SOURCE, never the environment, and exits 2
before the lock or the journal unless that path is absolute and ends in
.app/Contents/Resources/backstop.sh. install.sh passes the flag to the
staged copy codesign has checked. The LaunchAgent and uninstall.sh keep
using the installed app. A stopped install now says to rerun the
installer; a hand-run scripts/backstop.sh would ask the installed build
again.

Tests: source and --app upgrades over such a build resume with the staged
binary under the recovery lock and never run the old one. A failed,
unverifiable, malformed or late answer, or a staged build without the
interface, keeps the entries and the previous app and agent. --own-bundle
refuses copies outside a bundle's Contents/Resources and relative paths.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
krishhgg and others added 2 commits October 7, 2026 16:08
Conflict in scripts/backstop.sh, in the lock_shared check: keep #32's
"$STAT" (absolute /usr/bin/stat) inode helper and #50's comparison, so a
"stat" on PATH cannot make a foreign fd 9 look shared, and a shared fd 9
still skips the stale status-file cleanup that would delete a live
supervisor's files.

Everything else merged cleanly: #50's supervise_command and run_bounded
(the supervisor owns its job, ignores TERM and HUP, keeps fd 9 until it
reaps the command, TERM only, 125 keeps state and stops) sit beside #32's
marker, nonce, deadline, --own-bundle and pending-start handling.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…wn change

The round 14 independent review (P1 at 2b8028c) showed the restore proof
setting 0 over a SleepDisabled 1 another tool set while the dialog was up,
also when the start then ran past its deadline.

The root command now reads `pmset -g` itself after the nonce, deadline and
uid checks, before it changes anything. A 1 the journal does not claim, or
a failed read, exits 6 with nothing changed. The start passes whether the
journal already owns the 1 as item 5 of argv; with "1" both reads are
skipped. Root then runs `disablesleep 1`, and the proof (the user's
`sudo -k -n /usr/bin/pmset -a disablesleep 0`) undoes that change instead
of writing 0 over whatever it found. A failed proof makes root set 0
itself before it writes any output, then exit 5, so a closed dialog cannot
stop the restore with SIGPIPE. After a passing proof come the deadline
(4), a second read (6) and the final `disablesleep 1`.

Exits 3, 4, 6, 69 and 75 are the new `.refused` error and, like exit 5,
roll back with no pmset: each leaves no change of the command's own. Any
other status is still undone like an end.

Not closed: pmset has no compare-and-set, so a 1 set between the first
read and root's own 1, or while that 1 is in effect during the proof, is
still cleared, and sleep is off while the proof runs even without the
rule. The docs and .greptile/rules.md say so.

Tests: RootCommandTests cover the order, every sudo policy (cached,
listing only, PASSWD, deny, no rule, no root entry), foreign 1s at each
point on a fake clock, unreadable reads, journal ownership, a failing
root restore and a closed dialog; StartOwnershipEndToEndTests run the
real prompt, guard and manager against one fake machine. The backstop
test that holds the marker expects the new calls; its lock and undo
assertions are unchanged. testLockSharingIgnoresAStatOnPATH covers the
"$STAT" kept in the #50 merge.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread Sources/Insomnia/System/AdministratorPrompt.swift Outdated
krishhgg and others added 3 commits October 7, 2026 20:27
The round 17 independent review (needs changes at 42e42e5) found three
blockers. R1 (P0, Greptile 4213796939): root turned sleep off before the
passwordless restore was established, so a root shell that died, or a
root fallback that failed, could leave sleep off with no unattended way
back. R2 (P1): the temporary 1, the proof and root's fallback set 0 over
another tool's SleepDisabled 1, and with the dialog's output closed a
refusal died by SIGPIPE, lockf reported 70 and the start undid it like a
failure. R3 (P1): a read could use up the deadline, and the write still
followed at or past it.

The root command no longer runs the restore and has no temporary 1 or
fallback. Its only write is the last step, `pmset -a disablesleep 1`.
Before it, root drops to the user who pressed Start (`sudo -n -u "#$4"`)
and has that user's sudo answer three queries with an empty environment
but LC_ALL=C and stdin from /dev/null; none of them runs a command:

- `sudo -V` must show sudo 1.9.15 to 1.9.x with only the sudoers policy,
  I/O and audit plugins.
- `sudo -k -n -l` must list without a password and show no Runas or
  command-specific Defaults, which apply to the restore but not to a
  listing.
- `sudo -k -n -ll /usr/bin/pmset -a disablesleep 0` must print exactly
  the six lines of /etc/sudoers.d/insomnia's restore rule: that file,
  RunAsUsers root, Options !authenticate and nothing else, the restore
  line, and Matched with the restore line.

Anything else exits 5 with nothing written: an older or unknown sudo,
other plugins, a path-only or truncated answer, another file, run-as or
option, a denial, a failed root switch, a listpw policy that wants a
password. The command ignores SIGPIPE, so a refusal keeps its status
when the dialog's output is gone, and sets LC_ALL=C for every tool it
runs by absolute path. The clock is compared with the deadline after
the nonce check, after the sudo queries and right before the write;
equality refuses. `pmset -g` is read once, after the queries.

Exit 5 is still `.restoreNeedsPassword` and 3, 4, 6, 69 and 75 are still
`.refused`; both now mean nothing was written, so the rollback runs no
pmset. Error texts say "sleep was not turned off".

Not closed, and stated in README, SECURITY.md, the spec and
.greptile/rules.md: a 1 set between root's read and its write is taken
for Insomnia's own; a status lost to a crash or a signal is still
undone; a listing is not the restore (a rule removed later, a log sudo
cannot write, other groups); sudo outside 1.9.15 to 1.9.x, bound
Defaults, listpw=always and a later rule refuse every Start. The
.greptile/config.json requirement is kept; only its mechanism sentence
names the queries.

Tests: RootCommandTests and RootCommandSudoAnswerTests run the real
command and its three awk readers against source-derived answers from a
fake sudo for every policy, a fake clock for each call before the
write, foreign 1s at each point and closed output.
StartOwnershipEndToEndTests run the real prompt, guard and manager
against one fake machine. Tests that expected the temporary 1 or the
fallback now expect no write before the queries pass.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sudoers rule said the root command "establishes" the passwordless
restore. Its three sudo queries are policy answers that run nothing, so
the rule now says it checks the restore with them and that they are not
a run of the restore. The requirement is unchanged: no pmset when the
check fails, and sleep is never turned off while the restore is missing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rsions; record before the read

The round 19 review found two configurations the sudo answers accepted
while the restore was already blocked, and a failed start that could
clear another tool's SleepDisabled 1.

- Any /private/etc/sudo.conf refuses (5). sudo -V skips an approval
  plugin with no show_version, and macOS installs no sudo.conf, so
  without one only the built-in plugins load.
- /etc/pam.d/sudo must have exactly one session line, macOS's own
  "session required pam_permit.so" (5). sudo opens the session for a
  run, not for a listing.
- sudo -V must show 1.9.17p2, the version whose source the check was
  read against (5). The refusal says another version needs an Insomnia
  release checked against it, not that a reinstall helps.
- sudo -k -n -l may show only Defaults from an accepted list
  (environment, lecture, prompt, timestamp and logging on/off entries);
  anything else, a backslash, a tab or a bound Defaults header
  refuses (5) and names the entry.
- Before it reads pmset -g, the command replaces the marker's nonce
  with "<nonce> writing", as the user (7 if that fails), and puts the
  nonce back on refusals 4 and 6. After a failure the app reads the
  marker under its lock: the file this start wrote, still holding only
  the nonce, means no command reached the sleep setting, so the start
  is rolled back without pmset. Any other marker is undone as before.
  Relaunch, the backstop and uninstall do not read it.

Docs, review rules and release rows describe the new checks and what
they still cannot establish.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread README.md Outdated
…shed starts from it

The round 21 review found that a process running as the user could put
the bare nonce back into the marker after pmset's write, so a failed
start dropped the restore it owed, and that relaunch, backstop.sh and
uninstall.sh never read the record at all.

The record moves to /private/var/db/com.kgarg.insomnia/<uid>, which
install.sh creates through sudo as root's, mode 0644, 45 bytes, and
which only root can write. The root command checks the receipt and every
folder up to / by lstat, reads pmset -g, then writes "<nonce> writing"
in place with dd conv=notrunc,fsync and reads it back before its only
pmset write. It never writes the marker.

Start journals sleepOffAttempt (nonce, the restore owed before, the
receipt's and the marker's device:inode, the deadline). Every reader
that deletes the marker under its lock settles the start from the
receipt: the app on any transaction, backstop.sh (version 3) and
uninstall.sh. The start's session.json never resumes, a receipt that
shows no write keeps the earlier owed restore, and anything else runs
the restore. A settlement that cannot be written keeps the record,
restores sleep and refuses Start until it can.

README and SECURITY.md now describe the 120 s wait, the waits on a
running command, the agent's schedule and the receipt as the code does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread Sources/Insomnia/Store/SleepOffReceipts.swift Outdated
Comment thread scripts/backstop.sh Outdated
krishhgg and others added 3 commits October 8, 2026 06:42
…art, flush with F_FULLFSYNC

The round 23 review found that two Insomnia folders of one user share the
receipt, so a second start's line could hide the first start's write; that
a root command keeps the marker inode it opened after a reader deleted a
replacement; that a timed-out dialog could still be answered after its
start was settled; that a never-write settlement lost its evidence when
the journal could not be written; that dd's fsync is not a drive flush;
and that backstop.sh and uninstall.sh ran cat and head through PATH.

The receipt line is now "<nonce> <predecessor> writing|refused" (82
bytes). A start claims it under its lock through <uid>.released, the
user's own file, and gives the claim back only once settled, so a start
from another folder is refused while one is unsettled. The root command
takes an exclusive flock on the open receipt (lockf -s -t 10, exit 75)
before any check and holds it through pmset; the app, backstop.sh,
install.sh and uninstall.sh take the same lock before they claim, read or
remove it, and a busy or failed lock decides nothing. The command checks
the descriptor and the path against the claimed device and inode, writes
only while the receipt begins with the predecessor (exit 8), and writes
the line through /usr/bin/perl with F_FULLFSYNC, refusing before pmset
when perl or the flush fails.

A start's expires is at most 130 s after it wrote pending-start. Until
then a receipt that still holds the predecessor decides nothing unless the
dialog ended by itself: the start stays journaled with its claim, Starts
are refused and nothing is undone. After a timeout the app waits up to
15 s for expires. A settlement step that fails keeps the attempt with what
the receipt showed, so a never-write result is retried without pmset.

backstop.sh (version 4) and uninstall.sh call cat and head by fixed path.
uninstall.sh's settle_stop no longer aborts on an unset second argument,
which bash 3.2's EXIT trap had turned into exit 0.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Four conflicts, each resolved as a union of both sides:

- SessionManager.init keeps this branch's markerLockTimeout and
  receiptLockTimeout and main's keptRecheckDelay, keptRecheckAttempts,
  keptRecheckSlowDelay and bootSession.
- RuntimeState.CodingKeys has main's kept-display keys and this
  branch's sleepOffAttempt.
- The backstop patch map in RecoveryScriptTests has this branch's CAT,
  HEAD, RECEIPTS and RECEIPT_OWNER and main's MV.
- TestSupport.makeManager takes both sides' parameters, in that order,
  with both sides' notes.

Main's PrivateDisplayGuardTests did not build against this branch:
its AfterSwitchOffSleepGuard wrapper now forwards SleepGuarding as this
branch has it (checkSleepSettingForStart, disableSleep, enableSleep in
place of setSleepDisabled), and its direct SessionManager call passes
the harness's receipts.

Everything else merged without conflict. backstop.sh and uninstall.sh
check the journal with main's record_text_problems before a settlement
republishes state.json, a settlement runs before main's kept-brightness
handling, and uninstall.sh stops while an attempt stays, so a kept
state.json never holds one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…sions with sleep off

README, SECURITY.md, spec sections 1, 2 and 8, release-validation.md and
.greptile/rules.md now describe the receipt as the code has it: the
82-byte line with the predecessor, the receipt's lock, the claim in
<uid>.released, the perl write with F_FULLFSYNC, exit 8, the 130 s
answer window, which receipts show no write, which are undone at once
and which only after the window, and that an undecided start stays
recorded with Start refused. Release-validation rows are all Not run.

EarlierBootLowPowerClaimTests (from #43) seeded a still-valid session
from boot A without SleepDisabled 1 in the fake pmset. Under this
branch a relaunch never turns sleep off again and ends a session whose
sleep was turned back on, so three of its tests saw the session end at
reconcile. The seed now shows the 1, as LidActionsTests' resumed
sessions already do.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread scripts/install.sh Outdated
krishhgg added a commit that referenced this pull request Oct 8, 2026
…till what was read, under a root-only lock

/etc/sudoers.d/insomnia is one file for the whole Mac. install.sh read it
through sudo, judged whose it was, and wrote it in a later sudo call;
uninstall.sh read it and removed it the same way. Another account's
install.sh could write its rule in between, and the later write replaced
that account's grant, or the removal deleted it (Codex P1 on 38a9d1d,
which the PR body had wrongly called accepted).

Now the compare and the change are one `sudo /bin/bash -c` call. As root
it takes /var/run/insomnia-sudoers.lock with lockf (umask 077, a symlink
refused; /var/run is writable only by root and the daemon group), checks
the rule is still absent or the bytes this run read (cmp), and only then
writes or removes it. install.sh stages the new rule beside the old one
(a name with a dot, which sudo skips), makes it root:wheel 0440, checks
that copy with visudo and renames it over the rule; a failure removes
the copy. The root shell's script is the function's text after the
scripts' fixed tool paths, so nothing it runs comes from PATH. A rule
that changed since the read, or a lock held past LOCK_TIMEOUT_SECONDS,
stops the run: install.sh changes nothing, uninstall.sh keeps the rule
and the app, and both ask for a rerun. The sudo reads of the rule now use
fixed paths (/bin/test, /bin/cat, /usr/sbin/visudo).

The grants do not change, nothing new runs as root outside the two
scripts, and the other-account process stop still comes before the first
sudo call.

Tests: the fake sudo runs the transaction unprivileged only when the
rule and the lock in its script are inside the fixture, and a gate file
holds it between a run's read and its write. New tests cover two
accounts installing from two homes at once, an uninstall whose rule is
replaced while it waits, an uninstall racing another account's install,
the lock held by another run (install and uninstall), visudo rejecting
the staged copy, a failed rename, the shape of the transaction, #32's
three-command rule, and both scripts naming the same lock. The two race
tests fail on the previous scripts (rule overwritten, rule removed).

Docs: README, spec section 2, and two "Not run" rows for a real
two-account race and the root-side file modes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
krishhgg and others added 2 commits October 8, 2026 10:24
…old the sleep undo while a command may act

Round 26 (independent25 at e741a7f):

- R25-1: every settlement (app, backstop.sh, uninstall.sh) removes the
  start's session, then journals the decision with
  sleepOffAttempt.settled while the claim is still held, then gives the
  claim back and drops the record. A settled record is finished from its
  decision and never read against the receipt again; a failure or crash
  after the decision keeps it, holds nothing back, and is retried.
  backstop.sh declares version 5 and the app requires it.
- R25-2: an undecided start (receipt locked, or a dialog that can still
  be answered) holds the sleep undo at any time, whatever an earlier
  session owes; Low Power Mode, processes and audio still undo.
- R25-3: install.sh reads the receipt and the release file only under
  the receipt's lock, keeps a held claim (also right after it made the
  receipt), stops at a claim in an unsafe file, and checks everything
  again before it replaces a release file.
- R25-4: prepare_low_power_off reads through the fixed $HEAD.
- R25-5: the root command reads pmset -g again after the writing line
  is flushed and read back, before its last clock check and pmset.
- R25-6: README, SECURITY, spec, release validation, both Greptile rule
  sources and source comments describe the new order and its costs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n lock the receipt, settled sessions resume

Independent round 27 (needs changes at 93fb3dc), findings 2 to 8:

- uninstall.sh takes the receipt's lock and checks the release file
  before it removes anything, refuses the whole uninstall while another
  Insomnia folder holds a claim or the lock stays busy, and keeps the
  lock, checking again, until the shared rule, agent, bundle, receipt
  and release file are gone.
- install.sh makes the receipt 0600 with one ACL entry,
  "user:<name> allow read", and repairs the receipt of earlier builds in
  place. The app (acl(3)), the root command, backstop.sh, uninstall.sh
  and install.sh accept only that mode and entry, so no other account
  can open the receipt and hold its lock.
- Reconcile resumes the session of a settled start that went through
  while its claim or record cleanup fails, with Starts refused until the
  cleanup finishes.
- Recovery reads of the journal and session in backstop.sh and
  uninstall.sh use fixed paths for head, cat and tr.
- Messages and docs say what deleting the marker, the answer window and
  a "writing" line prove, and no more.
- supervise in install.sh and uninstall.sh ignores TERM and HUP with
  errexit off for the call's life; only the call gets the default
  actions back, so fd 9 is held until the call is reaped.
- uninstall.sh reads a private copy of state.json, carries every failed
  read to its decision, stops before removing anything when one fails,
  and removes state.json only while it equals the checked copy.
  backstop.sh's raw reader and conversion report failure too.

Finding 1 (F7) stays open and unwaived. The app that gets exit 6 but
cannot journal its rollback now keeps "never wrote" for that start in
every later settlement in the same process.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread scripts/uninstall.sh Outdated
Comment thread Sources/Insomnia/Core/SessionManager.swift Outdated
Comment thread SECURITY.md
krishhgg and others added 3 commits October 9, 2026 01:29
… calls stay supervised, finite session math

Independent round 29 review (GPT-6.1-Sol, xhigh) of d933b68: needs
changes, findings 1 to 13.

- Root command: a `refused` line that cannot be written is tried again,
  at most three times within 3 s, under the receipt's lock, before it
  exits (finding 1). F7 stays open.
- uninstall.sh: every read of the receipt, the release file and the
  journal keeps its bytes and its exit status apart, and a failed read,
  one file without the other, or a file or folder that fails the checks
  stops it before the bootout. `sudo -v`, then `sudo -n -v`, then every
  root command through bounded `sudo -n` under both locks; a failed,
  stopped or still-running call stops the run there (findings 2 and 4).
- install.sh repairs an earlier receipt only under its lock and while
  the release file shows no claim, the entry before the mode (finding 5).
- The app's ACL reader refuses a list it cannot read whole (finding 6).
- backstop.sh carries a later read's failure to its decision instead of
  taking it for absence (findings 7 and 8).
- A session.json whose extensions do not add up is moved aside like one
  that does not parse; a settled start's own session is told by its
  first end and start, with nothing that can trap; the menu keeps the
  cleanup line while it goes on (findings 9 and 10).
- Fixed paths for the remaining trust reads, no persisted sudo pid
  (finding 11). README, SECURITY.md, spec and release-validation.md
  match (finding 12). A fixture run whose output cannot be captured
  throws (finding 13).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…and rule text

The shared shape_of reader set the global `problems` to its lines. In
uninstall.sh that name is step 4's array, so a clean journal left one empty
element in it and every uninstall that reached step 4 stopped as incomplete,
before the LaunchAgent bootout. shape_of now sets shape_lines, in both
scripts, which stay byte-for-byte shared.

- Two backstop tests expect the publish failure's reason in the log line.
- The repair test's lock lists include the fake launchctl bootout's entry.
- .greptile/config.json: the install rule says a repair happens only while
  the release file shows no claim, entry before mode, under the receipt's
  lock when the user can open it; a held claim stops the install.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
testUninstallStopsWhenTheSettlementCannotReadTheJournalOrTheSession built
its expected messages from the setUp fixture's paths, then ran each case in
a fixture of its own, so every case failed on the path alone. The expected
text is now made from the case's fixture.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread scripts/backstop.sh Outdated
Comment thread scripts/uninstall.sh
krishhgg and others added 2 commits October 9, 2026 04:02
…blish from the live journal

Greptile on 7e233e8, a full disk: the backstop exited 1 when mktemp could
not make its folder for the copies it reads, so nothing was restored. It
now keeps those copies in memory (copy_private with COPY_IN_MEMORY=1, read
through plutil's standard input, plutil's messages kept in memory) with the
same checks, and a copy whose file cannot be written is made again in
memory. A NUL byte, which a shell variable cannot hold, is unknown (2).
uninstall.sh keeps COPY_IN_MEMORY=0 and stops on such a copy.

Greptile on 7e233e8, an interrupted uninstall: a stop between removing the
receipt and removing its release file left the release file alone, which
every rerun refused. Under the receipt's lock, just before it removes the
receipt, the uninstall now writes .uninstall-receipt-removal with the
release file's device, inode, change time to the nanosecond and line. A
rerun that finds the release file without the receipt finishes the removal
only while the record names that file exactly and it is free; anything
else stays refused.

The hosted failure on 7e233e8 (testBackstopKeepsAnOwnerACLAndStillUndoesTheJournal,
1 ACL entry expected, 0 found) was a regression from 468db03: it published
from the private copy, which carries no ACL. Every publish now copies the
live journal with cp, as main does, and requires the copy to hold the bytes
the run read (same_as_read), in backstop.sh and in uninstall.sh's edit_state.

Tests: the copy on its own (memory, RLIMIT_FSIZE, an unwritable folder), the
backstop with no folder or a folder nothing can be created in, publish
failures and a same-size rewrite during the read, an extended attribute as
a stand-in for the ACL, and the uninstall's record across a failed, stopped,
credential-less and still-running removal, with each change the record
must refuse. Docs: README, SECURITY, spec and release validation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o ACL without -p

r30-broad3 on 29949a6 failed two tests on their own fixtures.
testScriptsAndAppReadTheSameSessionDates copies the readers into a harness
and lacked plutil_on, which plutil_run now runs plutil through.
testUninstallStopsWhenTheSettlementCannotReadTheJournalOrTheSession faked
COPY_PERL with a reason on standard error; COPY_PERL prints it on standard
output since 29949a6, and the fake now does too. Expected messages are
unchanged.

29949a6's comments and spec said a cp of the live journal carries its
access control list. cp(1) copies extended attributes unless -X is given
and documents copying an ACL only with -p, which neither main nor these
scripts pass. The text now says so; the hosted ACL fixture keeps its entry
because its cp fails, as on main. No behavior change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Comment thread scripts/uninstall.sh
Comment on lines +2108 to +2111
if release_state && [[ "$release_word" == free ]] && progress_line && [[ "$progress_seen" == "$release_now" ]]; then
shared_seen="finishing $release_now"
release_seen="$release_nonce $release_word"
return 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Rerun deletes new install files

The new finishing path proceeds without holding the receipt’s lock. If this rerun uses another INSOMNIA_HOME, a standard installer has a different recovery lock and can recreate the shared receipt after the rerun’s last shared_unchanged() check. The removal loop then deletes the new receipt and .released by path, even while the installer holds the new receipt’s lock. That installation loses files required for Start and recovery.

Serialize this cleanup with receipt creation using a shared lock that survives receipt removal.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/uninstall.sh
Line: 2108-2111

Comment:
**Rerun deletes new install files**

The new `finishing` path proceeds without holding the receipt’s lock. If this rerun uses another `INSOMNIA_HOME`, a standard installer has a different recovery lock and can recreate the shared receipt after the rerun’s last `shared_unchanged()` check. The removal loop then deletes the new receipt and `.released` by path, even while the installer holds the new receipt’s lock. That installation loses files required for Start and recovery.

Serialize this cleanup with receipt creation using a shared lock that survives receipt removal.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Conductor Fix in Codex

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant