Guard the self-update pipeline against shipping broken Python - #34
Merged
Merged
Conversation
After a git pull, compile the core pulse files and scan them for leftover merge conflict markers. If either check fails, reset back to the pre-pull commit, surface a self-update-syntax-fail ledger entry, and skip the update so the next launchd restart can't crash-loop on broken code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Route the pulse LaunchAgent through bin/run-pulse.sh, which runs py_compile on bin/pulse.py before exec'ing it. On failure it logs to the launchd err file and exits 0, so a broken pulse can't crash-loop launchd into silent throttling. The plist template now runs the wrapper and passes the arch-resolved python3 as its argument. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Only flag a bare === line as a conflict marker when an arrow marker is also present, so an RST doc underline in a gate file can't false-trip the gate and revert a healthy pull. - Never let the gate's py_compile subprocess raise into the pulse. - Carry the auto-stash recovery hint into the syntax-fail ledger entry. - Fix run-pulse.sh and CHANGELOG wording: this LaunchAgent has no KeepAlive, so it fires every 300s regardless of exit code; a broken pulse.py can't self-heal from inside pulse.py; the wrapper reaches existing machines only after reload. - Add regression tests: RST underline doesn't trip the gate; wrapper good path with no extra args. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ht in Python Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…d narrow the pre-flight Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…symlinks, and docs Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
This PR stops code that won't parse, such as leftover conflict markers or a syntax error, from silently stopping the pulse. It covers code arriving in a pulled commit and code sitting in the working copy. Errors that only show up when the code runs, like a renamed function or a missing package, are out of scope. They still fail with only the launchd error log as a trace.
What actually happened in July 2026: conflict markers were left in the working copy of
bin/pulse.py, and the pulse failed with aSyntaxErroron every tick for months. The only trace wasassistant-pulse.launchd.err, which nobody reads. The markers were never committed.17f3862:bin/pulse.pycompiles, andgit log --all -G'^<<<<<<<'finds nothing. The original brief blamed a pulled merge commit, which was wrong. This PR guards both paths anyway.bin/self_update.py). It runs after the fetch and the "ahead" and "dirty" checks, and before any stash or merge.bin/,src/,hooks/,install/,prompts/,skills/,launchagents/,config/,docs/, andslack-reactor/, plus the installers. Symlinks and submodules are skipped.self-update-syntax-failledger entry names the file and says to pull by hand if the refusal is wrong.gate-errorand isn't remembered.git merge --ff-only <sha>.bin/run-pulse.py). The pulse LaunchAgent now runs a small Python wrapper.pulse.pyand the modules it imports at startup (assistant,assistant.model_tiers), then replaces itself with the pulse. The pulse keeps the same interpreter and arguments.~/.assistant/pulse-preflight.json. It then exits 0, as the brief asked.bin/render-assistant-page.py). While the recorded failure is newer than the last pulse, a red alert at the top of the page, outside the collapsed services section, shows "Pulse can't start: : SyntaxError: …". Thecom.assistant.assistant-pageagent redraws the page without the pulse. The existing pulse banner is unchanged.Why the design changed during review:
git reset --hardcould erase operator edits made mid-pull, and it broke the repo's "never reset" rule, so the check now runs before the pull..shfiles.Tests
tests/test_self_update.pyruns real throwaway git repos. It covers these cases:bin/,src/,hooks/, andinstall/, plus markers ininstall.sh. HEAD, the reflog, and the working tree stay unchanged.tests/files,README.md, binary files, and symlinks don't block a healthy update.tests/test_run_pulse_wrapper.pycovers these paths:~/.assistant.pulse.pymust be stdlib or listed in the wrapper's startup files. The plist must run the wrapper.tests/test_renderer_in_process.pycovers the top alert for a new failure and for no heartbeat, and checks that an old, corrupt, or missing record renders nothing.tests/test_renderer_brief_tab.pypins the clock in the focus-unpin test. It began failing onmainaround 2026-09-23, once its fixture alert passed the 4-day freshness window.Known limits
bin/run-pulse.py. The LaunchAgent then can't start the pulse until you switch back, and the banner goes stale.bin/run-pulse.pywhile the loaded LaunchAgent still points at it. After a revert, reload the agent's definition withlaunchctl bootout gui/$(id -u)/com.assistant.assistant-pulseand thenlaunchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.assistant.assistant-pulse.plist, or reboot.assistantpackage. A brokensrc/assistant/__init__.pystops the dashboard alert, and stops Slack once the listener restarts. The pre-flight still skips cleanly and logs to stderr.launchctl listshows no error. The dashboard banner is where the failure shows.Operational note (not in this diff)
Unloaded and disabled the dead
com.mukuls.assistant-todo-reviewLaunchAgent. It exited with code 78 because it pointed at~/.claude/bin/review-todo.py, which doesn't exist and has no history in this repo. It's renamed to.disabled-20260927.🤖 Generated with Claude Code