skills / Tooling & Setup
stats-sync
Sync caezium/stats fork with upstream exelban/stats. Rebases henry/history-mcp onto upstream/master, stops on any conflict, runs the feature-presence audit + Swift tests + Debug build, then halts at a pre-install checkpoint and waits for the user to say "install". Use when the user says "/stats-sync", "sync stats", or asks to update the Stats fork.
Stats Sync
Routine for keeping the caezium/stats fork up to date with
exelban/stats. The fork's value is everything in henry/history-mcp:
a long-running history DB, an MCP query server, a SwiftUI History view,
configurable retention/tier policy, reader energy gating, and the test
scaffold. None of that exists upstream — every commit on
henry/history-mcp is load-bearing and must survive every sync.
Conflict policy: stop on every conflict
Unlike dayflow-sync there is no per-commit yaml and no auto-skip class.
The fork has only ~15 commits and they're all features — there's nothing
safe to drop. On any rebase conflict: stop with full context and ask the
user how to resolve.
Pre-install checkpoint
After rebase + audit + tests + Debug build pass, the skill always
stops with a glanceable summary and waits for the user to reply
"install" (or "abort"). It never auto-touches /Applications/Stats.app
because the user's ~/Library/Application Support/Stats/lldb is real
data — multiple gigabytes of their accumulated history — and a botched
install loses it.
Context
- Local clone:
~/Desktop/stats - Fork remote (
origin):https://github.com/caezium/stats - Upstream remote (
upstream):https://github.com/exelban/stats - Upstream's default branch is
master(notmain— it's an older project) - Integration branch:
henry/history-mcp— this is what the user builds from. The fork has no equivalent of dayflow's "main mirrors upstream" pattern; we only track upstream by SHA in state. - State directory:
~/.claude/skills/stats-sync/state/last-synced-upstream.txt— last upstream/master SHA processedlast-synced-date.txtlast-skipped-commits.log— empty in practice (we never skip)
Steps
1. Sanity check
cd ~/Desktop/stats
git remote -v
git status --short
git branch --show-current
df -h ~ | tail -1
Required state before proceeding:
originpoints tohttps://github.com/caezium/stats.gitupstreampoints tohttps://github.com/exelban/stats.git- Working tree clean (no uncommitted changes, no untracked source files —
.claude/worktrees are fine) - Current branch is
henry/history-mcp(if not, ask the user before switching — they may be mid-feature on something else) - Free space on
~≥ 4 GB (Stats's lldb can be 1–2 GB and Xcode needs room for module cache + build_dist)
If the working tree is dirty, stop and ask. If disk < 4 GB, suggest the user clean their Spotify/swiftpm caches; do not delete anything automatically. The Stats lldb is precious.
2. Fetch upstream
cd ~/Desktop/stats
git fetch upstream --prune
git fetch origin --prune
Compare git rev-parse upstream/master against
~/.claude/skills/stats-sync/state/last-synced-upstream.txt.
If equal, nothing changed upstream — tell the user in one line and
exit. No rebase, no audit, no checkpoint.
3. Show what's new upstream
SINCE=$(cat ~/.claude/skills/stats-sync/state/last-synced-upstream.txt)
git log "$SINCE..upstream/master" --oneline --no-merges
git log "$SINCE..upstream/master" --no-merges --shortstat | tail -5
Summarize in plain English ("Upstream added N commits since vX.Y.Z —
mostly bug fixes for the GPU module" / similar). Also note any new tags
(git tag --contains upstream/master minus the previous synced tag).
Don't dump the full log into the user's reading view.
4. Rebase henry/history-mcp onto upstream/master
cd ~/Desktop/stats
git checkout henry/history-mcp
git rebase upstream/master
On any conflict:
- Capture context:
git status # which files CONFLICT_SUBJECT=$(git log -1 --format='%s' REBASE_HEAD) echo "Conflict during: $CONFLICT_SUBJECT" - For each conflicted file, show:
- Our hunks (
<<<<<<<to=======) - Their hunks (
=======to>>>>>>>) - Upstream's nearby intent:
git log upstream/master -p -1 -- <file>
- Our hunks (
- Always stop and surface the conflict to the user. Never
git rebase --skip— every commit onhenry/history-mcpis a feature that must survive. - After the user dictates the resolution:
git add <files>,git rebase --continue. If the rebase chains into another conflict, repeat from (1).
Common conflict files to flag with extra care (these are where the fork's most load-bearing patches live):
Kit/module/reader.swift— base Reader class withtick(),selfPersists,lastReadAt,historyCadenceSeconds,popupClosedIntervalMultiplier. Upstream often re-edits this.Modules/CPU/readers.swift— FrequencyReader Taskdeferfix.Modules/Net/main.swift+Modules/Net/readers.swift—selfPersists: truewiring on Net readers + setup() overrides for multiplier=1.Stats/Views/AppSettings.swift— History storage UX.Kit/plugins/DB.swift(entirely fork-introduced, conflicts unlikely unless upstream adds something at the same path).
If git rebase --abort becomes necessary, do it and stop. Don't try
again with -Xours/-Xtheirs — wrong-default merges silently destroy
features.
After a clean rebase:
git push origin henry/history-mcp --force-with-lease
5. Feature-presence audit
Every grep below must match. If any FAILS, a patch got lost during the rebase — stop and report which one. Don't push, don't proceed.
| Feature | Verify by |
|---|---|
| History DB exists | [ -f Kit/plugins/DB.swift ] |
| NetTierPolicy + tier configurability | grep -q "public struct NetTierPolicy" Kit/plugins/DB.swift |
| Drop sentinel for past-hour-tier rows | grep -q "dropSentinel" Kit/plugins/DB.swift |
| LLDB compactRange | grep -q "compactRange" Kit/lldb/lldb.h && grep -q "CompactRange" Kit/lldb/lldb.m |
| Hourly maintenance calls compactRange | grep -q "compactRange()" Kit/plugins/DB.swift |
| ttlDaysOverride test hook | grep -q "ttlDaysOverride" Kit/plugins/DB.swift |
| QueryServer / MCP | [ -f Kit/plugins/QueryServer.swift ] && grep -q "case \"/info\"" Kit/plugins/QueryServer.swift |
| QueryServer staleness | grep -q "age_seconds" Kit/plugins/QueryServer.swift |
| History view | [ -f Stats/Views/HistoryView.swift ] |
| History view: 30 d / 90 d range chips | grep -q '"30d"' Stats/Views/HistoryView.swift && grep -q '"90d"' Stats/Views/HistoryView.swift |
| Stride-sampled top-N | grep -q "findTimeSeriesSampled" Stats/Views/HistoryView.swift |
| Staleness warning chip | grep -q "staleReaderCount" Stats/Views/HistoryView.swift |
| Reader tick gating | grep -q "popupClosedIntervalMultiplier" Kit/module/reader.swift && grep -q "internal func tick" Kit/module/reader.swift |
| Reader 60 s history cadence | grep -q "historyCadenceSeconds" Kit/module/reader.swift |
| Reader selfPersists flag | grep -q "selfPersists" Kit/module/reader.swift |
| Reader lastReadAt diagnostics | grep -q "lastReadAt" Kit/module/reader.swift |
| FrequencyReader defer fix | grep -q "defer { self.isReading = false }" Modules/CPU/readers.swift |
| Net selfPersists wiring | grep -q "selfPersists: true" Modules/Net/main.swift |
| Net readers opt out of gate | grep -q "popupClosedIntervalMultiplier = 1" Modules/Net/readers.swift |
| History storage settings UX | grep -q "Keep detailed data for" Stats/Views/AppSettings.swift && grep -q "Keep summary data for" Stats/Views/AppSettings.swift |
| Option lists | grep -q "HistoryDetailedDaysOptions" Kit/types.swift && grep -q "HistorySummaryDaysOptions" Kit/types.swift |
| Test scaffold present | [ -f Tests/HistoryDB.swift ] |
| ReaderTickTests present | grep -q "final class ReaderTickTests" Tests/HistoryDB.swift |
Run these as a single bash block; collect failures into a list. Any non-zero exit means a patch got lost in the rebase.
6. Run the Swift tests
cd ~/Desktop/stats
xcodebuild -project Stats.xcodeproj -scheme Stats \
-configuration Debug -destination 'platform=macOS' \
-derivedDataPath /tmp/stats-sync-build \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO \
test 2>&1 \
| grep -E "error:|Test Case.*(failed)|TEST (SUCCEEDED|FAILED)|Executed [0-9]+ test" \
| tail -15
Expected: ** TEST SUCCEEDED ** with all tests passing (currently 27
total: 17 HistoryDBTests + 3 RAMTests + 7 ReaderTickTests; this number
will grow over time).
Note the trailing ** TEST FAILED ** line that sometimes appears
after a successful test run is from the test host binary trying to
open the live lldb while running Stats holds its LOCK. That's a
pre-existing nuisance, not a real failure — verify by the
Executed N tests, with 0 failures count just above it.
If real test failures appear: stop, don't push, don't install. Show the failure to the user.
7. Debug build (final verification before the install checkpoint)
cd ~/Desktop/stats
xcodebuild -project Stats.xcodeproj -scheme Stats \
-configuration Debug -destination 'generic/platform=macOS' \
-derivedDataPath /tmp/stats-sync-build \
CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO \
build 2>&1 \
| grep -E "error:|BUILD (SUCCEEDED|FAILED)" \
| tail -10
Expected: ** BUILD SUCCEEDED **. The test step (6) already builds
everything but a separate build invocation catches errors that only
surface for the non-test target.
If the build fails:
- Errors in files our patches touched → patch needs updating. Stop.
- Errors in files only upstream touched → upstream regression in this release. Tell the user; usually means waiting for the next upstream patch or porting a workaround.
8. Pre-install checkpoint
Always stop here. Do NOT proceed to step 9 unless the user replies
"install" or similar. This is the hard gate before
/Applications/Stats.app is touched.
Present a glanceable summary in this shape:
Stats sync — ready to install? (<upstream version tag>, <N> upstream commits)
Rebase: clean / <N> conflicts manually resolved
Conflicts on: <file paths if any>
Audit (~22 grep checks): all pass / N failed
Tests: ** TEST SUCCEEDED ** — <N> executed, 0 failures
Debug build: ** BUILD SUCCEEDED **
What changed upstream:
- <one bullet per upstream commit, summarized — e.g. "GPU fan curve fix">
- <if there's an upstream version tag in this range, name it>
Currently installed: <bundle version of /Applications/Stats.app, if installed>
Will be installed: Debug build from /tmp/stats-sync-build
Reply "install" to proceed with quit-Stats + install-to-/Applications
+ live-endpoint verify.
Reply "abort" to leave the rebased branch on the remote without
touching the live app.
If anything looks off (audit failure, suspicious upstream commit touching reader/DB code), explicitly recommend "abort" or "investigate first" rather than "install".
9. (On user "install") Quit + install + verify
Run only after the user explicitly says "install".
# 1. Quit currently-running Stats
osascript -e 'tell application "Stats" to quit' 2>/dev/null
sleep 3
pkill -9 -f Stats.app/Contents/MacOS/Stats 2>/dev/null || true
sleep 1
# 2. Replace /Applications/Stats.app
rm -rf /Applications/Stats.app
cp -R /tmp/stats-sync-build/Build/Products/Debug/Stats.app /Applications/Stats.app
xattr -cr /Applications/Stats.app
# 3. Launch
open /Applications/Stats.app
sleep 6
PID=$(pgrep -fl Stats.app/Contents/MacOS/Stats | head -1 | awk '{print $1}')
echo "Stats running: pid=$PID"
# 4. Free build cache
rm -rf /tmp/stats-sync-build
Note: no data backup is needed. The lldb format is forward-compatible
within this fork's schema — every commit on henry/history-mcp preserves
the on-disk layout. If a future commit ever changes that, add a backup
step here before it ships.
Verify the live endpoints (gives confidence the install survived):
sleep 4 # let QueryServer come up
curl -s http://127.0.0.1:9276/health
curl -s http://127.0.0.1:9276/info \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
readers = d.get('readers', [])
stale = [r for r in readers if r.get('age_seconds') is not None and r['age_seconds'] > 300]
print(f'prefixes={len(d[\"prefixes\"])}, readers={len(readers)}, stale_at_startup={len(stale)}')
"
Expected:
/healthreturns{"ok":true,"app":"Stats","port":9276}/inforeturns at least 10 prefixes;stale_at_startupmay be a few (some readers don't fire until a popup-open event), no warning yet.
If /health fails to respond after 8 seconds total, stop and report —
the QueryServer didn't come up. Probable causes: port collision, the
build is missing the QueryServer file (which would have failed step 5),
or auto-update tried to validate the install and bailed (the unsigned
build doesn't carry a team ID — see the alert message from past
sessions).
10. Bump state markers
After successful end-to-end run (install succeeded + endpoints verified):
git -C ~/Desktop/stats rev-parse upstream/master \
> ~/.claude/skills/stats-sync/state/last-synced-upstream.txt
date +%Y-%m-%d > ~/.claude/skills/stats-sync/state/last-synced-date.txt
State marker advances ONLY after install succeeds AND endpoints respond. Aborts, conflicts, audit failures, test failures, build failures, and QueryServer no-shows all leave the marker at the previous SHA so the next run retries from the same base.
11. Final report
Tell the user, under 200 words:
- Upstream version + N commits merged
- Conflicts: how many, which files (if any)
- Audit: pass/fail
- Tests: pass count
- Build: succeeded
- Install: bundle version, QueryServer responsive at the expected port
- Anything notable in the upstream changes (e.g. "GPU module gained a new metric — visible in your charts next time the GPU module is reloaded")
Failure modes
- Working tree dirty → ask the user before anything.
- Not on
henry/history-mcp→ ask, don't auto-switch. - Disk < 4 GB → stop, suggest cache cleanup, don't auto-delete.
- Rebase conflict → stop with full context, never
--skip. - Feature-presence audit failure → stop, name the missing patch.
- Test failures → stop, don't push, surface the failure.
- Build failure → stop, don't push, classify (our files vs upstream).
- No upstream changes → exit early, one-line report.
- Pre-install checkpoint → always stops here; not a failure.
/healthno-show after install → stop, report; the install binary may be broken or port-collided.