Skip to content

v1: shared client + calendar countdown + CI status integrations - #1

Merged
sumitake merged 25 commits into
mainfrom
dev/claude/v1-integrations
Aug 3, 2026
Merged

v1: shared client + calendar countdown + CI status integrations#1
sumitake merged 25 commits into
mainfrom
dev/claude/v1-integrations

Conversation

@sumitake

@sumitake sumitake commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Summary

  • Shared busybar Python package: config loader (deep-merged defaults, BUSYBAR_HOST override) and device HTTP client (draw/clear/status/BUSY snapshot) with a four-state draw result (drawn / rejected-409 / http-error / unreachable; only unreachable triggers loop backoff)
  • calendar_countdown: next-meeting countdown from macOS EventKit (macOS 14+), warning color near start, optional auto-BUSY during events, priority 20
  • ci_status: GitHub Actions status via REST only (GraphQL never touched) with per-repo ETag conditional requests (304s cost no quota), red failure alerts at priority 60 with LED fallback during BUSY sessions (priority 90), optional stuck-queued detection for self-hosted runners
  • launchd LaunchAgents (with PYTHONPATH hardening) + per-integration READMEs + root README
  • CI: pytest (36 tests, timezone-independent) + gitleaks secret scan

Design docs

  • Spec: docs/superpowers/specs/2026-08-03-calendar-ci-integrations-design.md
  • Plan: docs/superpowers/plans/2026-08-03-busybar-integrations-v1.md

Test plan

  • Full suite green: uv run pytest — 36/36 (any timezone)
  • Device smoke test: draw/status/clear against BUSY Bar at 10.0.4.20 (DrawResult.DRAWN)
  • ci_status --once --dry-run against a real repo (correct green-state payload)
  • Sanitization sweep: no personal data beyond the documented USB-default IP
  • Calendar first-run permission grant + live countdown (needs operator terminal)

🤖 Generated with Claude Code

sumitake and others added 22 commits August 3, 2026 14:04
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 249c564e04

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +23
if runs is not None: # None = 304/no-change/error -> keep cached state
state_cache[repo] = evaluate_runs(repo, runs, now,
c["stale_queued_minutes"])

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 Badge Re-evaluate queued runs after 304 responses

When a run is first fetched before stale_queued_minutes elapses, the cached RepoState records it as non-stuck; subsequent 304 responses skip evaluate_runs, even though the run's locally computed age continues increasing. A run that remains queued can therefore never produce the promised stuck alert until GitHub changes the representation—typically when it leaves the queued state. Cache the runs or queued timestamps and re-evaluate their age on every poll.

Useful? React with 👍 / 👎.

Comment on lines +40 to +41
resp = requests.get(url, headers=headers,
params={"per_page": 10}, timeout=(5, 15))

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 Badge Preserve failures that fall outside the first results page

For repositories with more than ten recent runs, a failed workflow disappears from this response as soon as other workflows produce ten newer runs. run_once then replaces that repository's cached state using only this truncated page and clears the alert even though the offending workflow never went green. Paginate, query each workflow, or retain per-workflow state until a newer run for that workflow is observed.

Useful? React with 👍 / 👎.



def _element(text: str, color: str, timeout_s: int) -> dict:
return {"id": "ci", "type": "text", "text": text, "font": "normal",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Sanitize CI text before sending it to the display

When a GitHub workflow name contains Unicode, such as an emoji or non-Latin text, this element forwards it unchanged even though the device text API accepts printable ASCII only. The resulting draw can be rejected, suppressing the CI failure alert entirely; apply the same ASCII sanitization used by the calendar integration before constructing the element.

Useful? React with 👍 / 👎.

Comment on lines +16 to +19
<key>EnvironmentVariables</key>
<dict>
<key>PYTHONPATH</key>
<string>__REPO__/src</string>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add the GitHub CLI directory to the LaunchAgent PATH

When this plist is installed as documented and gh comes from Homebrew (commonly /opt/homebrew/bin/gh), launchd does not inherit the user's interactive-shell PATH. Although uv is invoked through an absolute placeholder, get_token() launches gh by name, so the agent reports that GitHub CLI is missing and KeepAlive repeatedly restarts it. Populate PATH with the resolved gh directory or substitute an absolute gh path during installation.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c08c782c22

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

if runs is not None: # None = 304/no-change/error -> keep cached state
state_cache[repo] = evaluate_runs(repo, runs, now,
c["stale_queued_minutes"])
payload = build_ci_payload(list(state_cache.values()), c["show_green"], timeout_s)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Avoid clearing alerts before every repository has a state

When a repository's first request fails (for example, a timeout, 401, or 500), fetch_runs returns None and that repository is absent from state_cache; building the payload from only the cached values can therefore display CI ok or call clear despite the unevaluated repository still having failures. This is especially harmful after a process restart because a transient GitHub error can immediately remove an alert left on the device by the previous process; defer the green/clear decision until every configured repository has a known state.

Useful? React with 👍 / 👎.

Comment on lines +35 to +37
busy = client.get_busy() or {}
if busy.get("type") in (None, "NOT_STARTED"):
client.set_busy_simple(remaining_ms)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not treat a failed BUSY lookup as an inactive session

When get_busy() returns None because its GET timed out or received a non-200 response, or {} makes the subsequent check treat the state as inactive. If the following PUT succeeds, auto_busy can overwrite an existing user BUSY session with the calendar event's timer; only call set_busy_simple after a successful lookup explicitly reports an inactive state.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9c507413c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +21 to +22
for r in runs: # API returns newest first; keep first seen per workflow
latest.setdefault(r["workflow_id"], r)

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 Badge Keep failures visible until a rerun completes successfully

When a failed workflow is rerun, the newest run is commonly queued or in_progress with no conclusion, so keeping only that entry causes the prior completed failure to disappear immediately even though nothing has gone green. The alert is then cleared for the entire rerun and remains clear if that run is cancelled; retain the last failing result until a newer run completes successfully.

Useful? React with 👍 / 👎.

Comment on lines +50 to +53
if "ETag" in resp.headers:
self._etags[repo] = resp.headers["ETag"]
try:
return resp.json().get("workflow_runs", [])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Cache the ETag only after parsing the response

If a 200 response carries an ETag but its body is malformed, this stores the ETag before resp.json() fails. Subsequent polls can therefore receive 304 for the unprocessable representation and indefinitely retain an old or missing repository state until GitHub changes the resource; parse and validate the workflow-runs payload before committing its ETag.

Useful? React with 👍 / 👎.

@sumitake
sumitake merged commit e04ca70 into main Aug 3, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant