Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions docs/bakhyard-merge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Moving nudge into Bakhyard

Draft, 2026-10-28, for the Bakhyard session to take over. The user's choices (2026-10-28): there is one app, not two; nudge becomes a Bakhyard service plus a page in the Bakhyard app, and this repo is archived; agents reach it through the gateway on a tailnet-only route that skips Google sign-in; the client CLI (`ask`, `mcp`, `setup`) and the skill live in the Bakhyard repo, which is public.

Until the cutover step, the nudge in the cluster (`cluster/nudge`) keeps serving: other sessions ask through it every day.

## What goes where

| Today (this repo) | In Bakhyard |
| --- | --- |
| `internal/inbox`, `sessions`, `agentdoor` (questions, notes, the held request) | `services/nudge`, a service behind the gateway like `todos` and `pulls` |
| `internal/phonedoor`: the page, the answer forms, the Sessions section | A route of the Svelte app (`/ask`), over `/api/nudge/...` |
| `internal/push`, `vapid`, `subscriptions` | The planned `notify` service (decision 64): VAPID key in a Secret, subscriptions in Postgres (`push_subscriptions`), one internal "send" call |
| The Tailscale lock (`Tailscale-User-Login`) | The gateway's: Google sign-in for `/api`, tailnet login alone for `/agent/` |
| `nudge ask`, `mcp`, `setup`, the skill | A `main` package in the Bakhyard repo (say `tools/nudge`), built and released from there |
| `cluster/nudge`, the image, the Gitea mirror of this repo | Gone: the service gets Bakhyard's manifests and image pipeline |

## The requirement: no refresh when the app is open

A question, a report or an answer must show up in an open app without a refresh. Two layers, both needed:

1. **Push while the app is open.** The service worker posts a message to every open window of the app on each `push` event, and the page refetches on it. The notification is still shown: iOS revokes a subscription whose pushes show nothing, so a silent push isn't an option.
2. **A live stream.** `GET /api/nudge/events` is a server-sent events stream that says "something changed"; the page refetches `/api/nudge/questions` on each event. It is the "step that needs faster updates" that Bakhyard's decisions 43 and 49 wait for. The gateway already flushes streams at once. Fallbacks: the 60 s poll while visible, and a refetch when the page becomes visible again, because iOS suspends a Home Screen app's connections in the background.

Home's "Needs you today" feed also lists the waiting questions (one row each, linking to `/ask`), through the same attention events as the other apps.

## Agents

- Agents on any machine call `https://<bakhyard host>/agent/questions` and `/agent/sessions` (today's paths). The gateway checks the tailnet login header only, for `/agent/`, and sends nothing else past the lock.
- A question is held open for as long as its request. The gateway must not time those requests out (Bakhyard's decision 41 leaves hung upstreams to the gateway), and must not for the event stream either. Nudge's check of the operator's proxy (risk 12) showed 330 s; hours are still untested.
- `nudge setup SERVER` takes the Bakhyard host. Every machine runs it once again at the cutover.

## Steps

Each is one small PR, in the Bakhyard repo unless noted.

| Id | Step |
| --- | --- |
| `notify-service` | `notify`: the VAPID key, subscriptions in Postgres, `POST` to send; the app's "turn on notifications" button and the service worker |
| `nudge-service` | `services/nudge`: questions, held requests and sessions in memory, moved with their tests; behind the gateway at `/api/nudge` |
| `agent-route` | The gateway's `/agent/` route: tailnet login only, no timeout on held requests |
| `ask-page` | The `/ask` route: pending questions, answer forms, Sessions |
| `ask-live` | The event stream, the worker's message to open windows, the visibility refetch (the requirement above) |
| `ask-feed` | Waiting questions in Home's "Needs you today" |
| `nudge-push` | A new question calls `notify`'s send |
| `client-move` | `tools/nudge`: `ask`, `mcp`, `setup` and the skill; `go install` path and releases from Bakhyard |
| `deploy-nudge` | Bakhyard's manifests carry the service and the route; the Flux image automation covers it (homelab repo) |
| `nudge-cutover` | Each machine runs `nudge setup`; the phone enables notifications in Bakhyard and the nudge Home Screen app goes; `cluster/nudge`, its namespace, PVC and Secrets, the Gitea mirror and the image go; this repo is archived (homelab repo, then here) |

`notify-service` comes first because Bakhyard's reminders need it too.

## Risks and open questions

1. **Questions are lost when a pod restarts, and Bakhyard rolls out often.** Flux rolls a service out on each push of its image. The nudge service has its own image, so only changes to it or to the gateway drop waiting questions, but the gateway also carries every other app's changes. Options: keep questions in memory (as today, and AGENTS.md's rule here), or keep pending questions in Postgres so a restart loses nothing and the agent's request re-attaches. For the user to decide.
2. **A phone subscription belongs to one origin.** The iPhone subscribes again from the Bakhyard app; the old subscriptions and VAPID key aren't carried over.
3. **One lock for two doors.** The `/agent/` route skips Google sign-in, so it must serve only those two paths and answer 403 without the tailnet login, with a test for each.
4. **Quality gates differ.** Bakhyard's file and function limits, its web gates (Biome, Playwright) and its Sonar rules apply to the moved code; the nudge page's vanilla JavaScript is rewritten in Svelte, not copied.
5. **The cluster Secret `nudge`** (the user's login) has no counterpart: Bakhyard's gateway already knows the user.
3 changes: 3 additions & 0 deletions docs/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ This document is the source of truth for scope and the work breakdown. How we wo
| `cutover` | The cluster serves the phone and the agents (checked 2026-10-27). The Mac's brew service, `tailscale serve`, formula and tap are retired; the binary comes from `go install`. Left for the user: delete the GitHub repo `codedogapp/homebrew-tap` (`gh auth refresh -s delete_repo`, then `gh repo delete`) | In review |
| `multi-device` | The steps above, together: the user's end check that a question from a second machine reaches the iPhone (decisions 32, 34 and 35) | Todo |
| `swift-spike` | A spike on a native iOS app (decision 32), findings in a docs PR | Done (#39): [swift-spike.md](swift-spike.md), desk research only |
| `bakhyard-merge` | The plan for moving nudge into Bakhyard ([bakhyard-merge.md](bakhyard-merge.md), decision 38); the steps are Bakhyard's, and its session takes them over | In review |

**Ideas**: answer Copilot CLI's permission prompts from the phone; a button that declines a question; several questions in one `nudge ask`; a history of answers; a notification that repeats until it's answered; take a notification away once its question is answered; a notification that opens its own question; a Copilot CLI plugin that bundles the skill and the MCP server (whether a plugin's MCP config takes a `timeout` is unchecked).

Expand Down Expand Up @@ -169,6 +170,7 @@ sequenceDiagram
35. **Multi-device, where the server runs**: the user's choices (2026-10-26): they accept agents on other machines reaching the server through the tailnet door, with the same Tailscale login on every machine (AGENTS.md's two-doors rule is amended). The server will run in an always-on local Kubernetes cluster on another laptop, and the tailnet will reach the whole cluster, so the Tailscale Kubernetes operator (or a sidecar) can publish it and add the login headers. Consequences, my design for the user's review: the VAPID key and the settings come from a file or the environment where there is no Keychain (a Kubernetes Secret), the subscriptions need a small volume or devices subscribe again after a restart, and the image is built from this repo. The Mac's `brew services` install stays for a single-machine setup. The agent API lies under `/agent/` on the phone door and is no different for a local agent that chooses `NUDGE_SERVER`.
36. **Hosting in the homelab** (the user's choices, 2026-10-27): the k3s homelab is ready (single node, `linux/amd64`, Flux from GitHub, Gitea with a registry and BuildKit, the Tailscale operator). Plan, my design for the user's review: the image is built by a Gitea workflow from the mirror of this repo and lives in Gitea's registry; the manifests are plain YAML in the homelab repo's `cluster/nudge/` (that repo's convention), written by me and reviewed by the user; an Ingress of class `tailscale` publishes the page on `https://nudge.<tailnet>.ts.net`, so `NUDGE_HOST` becomes that name and devices subscribe again at the new address; one replica with the `Recreate` strategy, because the server keeps questions in memory; a small volume holds the VAPID key and the subscriptions (`NUDGE_VAPID_KEY_FILE`, `NUDGE_SUBSCRIPTIONS`); client machines get the server from a `server` key in their settings file, which `nudge ask` and `nudge mcp` read. After the user confirms the cluster works, the Mac's service and `tailscale serve` are retired.
37. **Homebrew retired** (the user's choices, 2026-10-27): with the server in the cluster, nothing runs on a Mac but `nudge mcp` and `nudge ask`, so the tap, the formula and `brew services` go, and clients install with `go install github.com/codedogapp/nudge@latest` (the user chose it over release binaries and over an HTTP MCP endpoint). `nudge setup SERVER` replaces the Mac setup of decision 33: no `tailscale serve`, no settings from `tailscale status`; the skill is embedded in the binary, since no formula installs its folder. Decisions 33 and the Homebrew facts and risk 11 are history. The Mac's Keychain VAPID key and the local subscriptions file were deleted; the cluster has its own key, so each phone subscribed again.
38. **One app: nudge moves into Bakhyard** (the user's choices, 2026-10-28): there is one app, not two. Nudge becomes a Bakhyard service and a page of its app, and this repo is archived. Agents reach it through the gateway on a tailnet-only route that skips Google sign-in. The client CLI and the skill move to the Bakhyard repo. An open app must show a new question without a refresh. The plan is in [bakhyard-merge.md](bakhyard-merge.md); the Bakhyard session takes it over, and nudge keeps serving until its cutover step.

## 5. Risks

Expand All @@ -195,6 +197,7 @@ sequenceDiagram
6. Multi-device: the work laptop is on the same tailnet (the user, 2026-10-25). What may run on it (Homebrew, Tailscale, a service) is still open, and so is the next question.
7. Multi-device, the tailnet door for agents: accepted by the user on 2026-10-26 (decision 35).
8. ~~Container details~~ Answered (2026-10-27): the operator is installed; the image lives in Gitea's registry; the manifests are plain YAML in the homelab repo (decision 36).
9. The move into Bakhyard (decision 38): whether waiting questions survive a restart, in Postgres, or stay in memory ([bakhyard-merge.md](bakhyard-merge.md), risk 1).

## 7. Sources

Expand Down
Loading