diff --git a/docs/superpowers/plans/2026-06-01-associate-connection-with-current-event.md b/docs/superpowers/plans/2026-06-01-associate-connection-with-current-event.md
new file mode 100644
index 0000000..b543ef4
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-01-associate-connection-with-current-event.md
@@ -0,0 +1,540 @@
+# Associate a Connection With Current Event — Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** On a connection's profile page, let a checked-in ATProto user tag that person's `quest.atmo.connection` PDS record with the event they're currently checked into, reversibly and in place.
+
+**Architecture:** A new `connection.SetEvent` updates an existing connection record in place via `com.atproto.repo.putRecord` (reusing the rkey, mirroring `event.Update`). The connection `View` handler resolves the current check-in and the target's most-recent connection record, passing both to the template, which renders a checkbox. A new `POST /connections/{did}/event` endpoint, driven by a small client script, performs the toggle — re-deriving the event server-side and never trusting the client for anything but the boolean.
+
+**Tech Stack:** Go, chi router, templ, indigo atproto OAuth client, SQLite, vanilla JS.
+
+---
+
+### Task 1: `connection.SetEvent` + record-finding helpers
+
+**Files:**
+- Modify: `internal/connection/connection.go`
+- Modify: `internal/connection/list.go`
+- Test: `internal/connection/connection_test.go`
+- Test: `internal/connection/list_test.go` (create if absent)
+
+- [ ] **Step 1: Write failing tests for the record-value helper and target finder**
+
+Add to `internal/connection/connection_test.go`:
+
+```go
+func TestBuildConnectionValue_OmitsEmptyEvent(t *testing.T) {
+ v := buildConnectionValue(syntax.DID(didA), time.Date(2026, 6, 1, 12, 0, 0, 0, time.UTC), "")
+ if v["$type"] != NSID {
+ t.Errorf("missing $type, got %v", v["$type"])
+ }
+ if v["with"] != didA {
+ t.Errorf("with = %v, want %s", v["with"], didA)
+ }
+ if _, ok := v["event"]; ok {
+ t.Error("event should be omitted when empty")
+ }
+}
+
+func TestBuildConnectionValue_IncludesEvent(t *testing.T) {
+ v := buildConnectionValue(syntax.DID(didA), time.Now().UTC(), "at://did:plc:x/quest.atmo.event/abc")
+ if v["event"] != "at://did:plc:x/quest.atmo.event/abc" {
+ t.Errorf("event = %v", v["event"])
+ }
+}
+
+func TestSetEvent_RejectsNilSession(t *testing.T) {
+ err := SetEvent(context.Background(), nil, "at://did:plc:x/quest.atmo.connection/r1", syntax.DID(didA), time.Now(), "")
+ if err == nil || !strings.Contains(err.Error(), "nil oauth session") {
+ t.Fatalf("expected nil session error, got %v", err)
+ }
+}
+
+func TestSetEvent_RejectsBadURI(t *testing.T) {
+ sess := &oauth.ClientSession{Data: &oauth.ClientSessionData{AccountDID: syntax.DID(didB)}}
+ err := SetEvent(context.Background(), sess, "", syntax.DID(didA), time.Now(), "")
+ if err == nil || !strings.Contains(err.Error(), "rkey") {
+ t.Fatalf("expected rkey error, got %v", err)
+ }
+}
+```
+
+Add to `internal/connection/list_test.go` (create the file with the same package and imports if it does not exist — `package connection`, importing `testing`, `time`, and `github.com/bluesky-social/indigo/atproto/syntax`):
+
+```go
+func TestFindForTarget_PicksMostRecent(t *testing.T) {
+ target := syntax.DID(didA)
+ older := ListEntry{URI: "at://x/c/old", With: target, ConnectedAt: time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC), EventURI: "at://x/e/1"}
+ newer := ListEntry{URI: "at://x/c/new", With: target, ConnectedAt: time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC), EventURI: "at://x/e/2"}
+ other := ListEntry{URI: "at://x/c/z", With: syntax.DID(didB), ConnectedAt: time.Now()}
+
+ got, ok := FindForTarget([]ListEntry{older, other, newer}, target)
+ if !ok {
+ t.Fatal("expected to find target")
+ }
+ if got.URI != "at://x/c/new" {
+ t.Errorf("URI = %s, want at://x/c/new", got.URI)
+ }
+}
+
+func TestFindForTarget_NotFound(t *testing.T) {
+ _, ok := FindForTarget([]ListEntry{{With: syntax.DID(didB)}}, syntax.DID(didA))
+ if ok {
+ t.Error("expected not found")
+ }
+}
+```
+
+Note: `didA` and `didB` are already defined as test constants in the `connection` package (used by `connection_test.go`). If `list_test.go` is a new file in the same package they are visible; do not redefine them.
+
+- [ ] **Step 2: Run the tests to verify they fail**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go test ./internal/connection/ -run 'BuildConnectionValue|SetEvent|FindForTarget' -v`
+Expected: FAIL — `undefined: buildConnectionValue`, `undefined: SetEvent`, `undefined: FindForTarget`.
+
+- [ ] **Step 3: Implement `FindForTarget` in `list.go`**
+
+Append to `internal/connection/list.go`:
+
+```go
+// FindForTarget returns the most-recent connection entry for the given target
+// DID. When several records exist (e.g. across multiple events), the one with
+// the latest ConnectedAt wins — matching the dedup the list/profile pages show.
+// Returns ok=false when the target has no connection record.
+func FindForTarget(entries []ListEntry, target syntax.DID) (ListEntry, bool) {
+ var best ListEntry
+ found := false
+ for _, e := range entries {
+ if e.With != target {
+ continue
+ }
+ if !found || e.ConnectedAt.After(best.ConnectedAt) {
+ best = e
+ found = true
+ }
+ }
+ return best, found
+}
+```
+
+- [ ] **Step 4: Implement `buildConnectionValue` and `SetEvent` in `connection.go`**
+
+Add the `nsidPutRecord` constant alongside the existing constants in `connection.go`:
+
+```go
+ // putRecord is the XRPC procedure for in-place updates.
+ nsidPutRecord = "com.atproto.repo.putRecord"
+```
+
+Append to `internal/connection/connection.go`:
+
+```go
+// buildConnectionValue assembles the record body for a quest.atmo.connection
+// record. The event field is included only when eventURI is non-empty.
+func buildConnectionValue(with syntax.DID, connectedAt time.Time, eventURI string) map[string]any {
+ value := map[string]any{
+ "$type": NSID,
+ "with": with.String(),
+ "connectedAt": connectedAt.UTC().Format(time.RFC3339),
+ }
+ if eventURI != "" {
+ value["event"] = eventURI
+ }
+ return value
+}
+
+// SetEvent rewrites an existing connection record in place (putRecord, reusing
+// the record's rkey) so its event association becomes eventURI. Passing an
+// empty eventURI clears the association. The caller carries the record's
+// current `with` and `connectedAt` (from the list entry) so no fields are
+// dropped. The at-uri stays stable because the rkey is reused.
+//
+// Caller must hold the repo:quest.atmo.connection OAuth scope.
+func SetEvent(ctx context.Context, sess *oauth.ClientSession, recordURI string, with syntax.DID, connectedAt time.Time, eventURI string) error {
+ if sess == nil {
+ return errors.New("connection: nil oauth session")
+ }
+ rkey := rkeyFromURI(recordURI)
+ if rkey == "" {
+ return fmt.Errorf("connection: cannot parse rkey from %q", recordURI)
+ }
+ if connectedAt.IsZero() {
+ connectedAt = time.Now().UTC()
+ }
+
+ input := map[string]any{
+ "repo": sess.Data.AccountDID.String(),
+ "collection": NSID,
+ "rkey": rkey,
+ // `validate` omitted — see Put for rationale.
+ "record": buildConnectionValue(with, connectedAt, eventURI),
+ }
+ if err := sess.APIClient().Post(ctx, syntax.NSID(nsidPutRecord), input, nil); err != nil {
+ return fmt.Errorf("putRecord %s: %w", NSID, err)
+ }
+ return nil
+}
+
+// rkeyFromURI extracts the record key (last path segment) from an at:// URI.
+func rkeyFromURI(uri string) string {
+ if uri == "" {
+ return ""
+ }
+ parts := strings.Split(uri, "/")
+ return parts[len(parts)-1]
+}
+```
+
+Add `"strings"` to the import block in `connection.go` if it is not already imported.
+
+- [ ] **Step 5: Run the tests to verify they pass**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go test ./internal/connection/ -v`
+Expected: PASS (all connection tests, including the new ones).
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /Users/brittany/Documents/Collective/atmoquest
+git add internal/connection/
+git commit -m "feat(connection): SetEvent for in-place event tagging + FindForTarget"
+```
+
+---
+
+### Task 2: Add fields to `ProfileView` and render the checkbox
+
+**Files:**
+- Modify: `features/connections/pages/profile.templ`
+
+- [ ] **Step 1: Add the new fields to the `ProfileView` struct**
+
+In `features/connections/pages/profile.templ`, extend the struct (after `FollowUp bool`):
+
+```go
+ FollowUp bool // follow-up flag from SQLite
+ // Current-event association (only populated for ATProto viewers who are
+ // checked into an ongoing event AND have a PDS connection record here).
+ CurrentEventURI string // at-uri of the ongoing event, "" if not checked in
+ CurrentEventName string // display name for the checkbox label
+ ConnRecordURI string // at-uri (rkey) of the record to update, "" if none
+ AssociatedWithCurrent bool // initial checked state
+```
+
+- [ ] **Step 2: Render the checkbox inside the notes section**
+
+In the same file, immediately after the `` that closes the follow-up toggle (around the `pv-followup-toggle` block) and before the closing `` of `pv-notes-section`, add:
+
+```go
+ if v.CurrentEventURI != "" && v.ConnRecordURI != "" {
+
+ }
+```
+
+- [ ] **Step 3: Add the script include**
+
+In the same file, next to the existing `` line, add below it:
+
+```go
+
+```
+
+- [ ] **Step 4: Regenerate the templ Go file**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go tool templ generate -f features/connections/pages/profile.templ`
+Expected: clean output, `features/connections/pages/profile_templ.go` updated.
+
+- [ ] **Step 5: Verify it builds**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go build ./...`
+Expected: silent success.
+
+- [ ] **Step 6: Commit**
+
+```bash
+cd /Users/brittany/Documents/Collective/atmoquest
+git add features/connections/pages/profile.templ features/connections/pages/profile_templ.go
+git commit -m "feat(connections): render current-event association checkbox"
+```
+
+---
+
+### Task 3: Populate the new fields in the `View` handler
+
+**Files:**
+- Modify: `features/connections/handlers.go` (the `View` method, around lines 420–445)
+
+- [ ] **Step 1: Wire current-event + record lookup into `View`**
+
+In `features/connections/handlers.go`, the `View` method already computes
+`viewerPDS := h.lookupPDSForDID(r, viewerDID)` and
+`entries, err := connection.List(r.Context(), viewerPDS, viewerDID)` and then
+loops over `entries` to set `view.ConnectedAt` / `view.EventName`.
+
+Immediately **after** that existing loop (after the `for _, e := range entries { ... }` block that sets `ConnectedAt`), add:
+
+```go
+ // Current-event association: only for ATProto viewers checked into an
+ // ongoing event who have a PDS connection record with this target.
+ if evURI, ok, cErr := checkin.Current(r.Context(), h.DB, viewerDID); cErr == nil && ok && evURI != "" {
+ if rec, found := connection.FindForTarget(entries, target); found {
+ view.CurrentEventURI = evURI
+ view.ConnRecordURI = rec.URI
+ view.AssociatedWithCurrent = rec.EventURI == evURI
+ if ev, gErr := event.Get(r.Context(), h.DB, evURI); gErr == nil {
+ view.CurrentEventName = ev.Name
+ }
+ }
+ }
+```
+
+`checkin`, `connection`, and `event` are already imported in this file. No new imports.
+
+- [ ] **Step 2: Verify it builds**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go build ./...`
+Expected: silent success.
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /Users/brittany/Documents/Collective/atmoquest
+git add features/connections/handlers.go
+git commit -m "feat(connections): pass current-event association data to profile view"
+```
+
+---
+
+### Task 4: `POST /connections/{did}/event` endpoint + route
+
+**Files:**
+- Modify: `features/connections/handlers.go` (new `SetConnectionEvent` method)
+- Modify: `features/connections/routes.go`
+
+- [ ] **Step 1: Add the handler method**
+
+Append a new method to `features/connections/handlers.go`:
+
+```go
+// SetConnectionEvent handles POST /connections/{did}/event — toggles whether
+// the viewer's connection record with {did} is tagged with the event the
+// viewer is currently checked into. Body: {"associate": bool}.
+//
+// The event is always re-derived server-side from checkin.Current; the client
+// only sends the boolean. Updates the most-recent connection record in place.
+func (h *Handlers) SetConnectionEvent(w http.ResponseWriter, r *http.Request) {
+ viewerDID, sess, ok := h.Auth.RequireSession(w, r)
+ if !ok {
+ return // RequireSession wrote the redirect/response
+ }
+
+ targetStr, err := url.PathUnescape(chi.URLParam(r, "did"))
+ if err != nil {
+ http.Error(w, "invalid DID", http.StatusBadRequest)
+ return
+ }
+ target, err := syntax.ParseDID(targetStr)
+ if err != nil {
+ http.Error(w, "invalid DID", http.StatusBadRequest)
+ return
+ }
+
+ var body struct {
+ Associate bool `json:"associate"`
+ }
+ if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 4*1024)).Decode(&body); err != nil {
+ http.Error(w, "invalid JSON", http.StatusBadRequest)
+ return
+ }
+
+ // Re-derive the current event server-side.
+ eventURI := ""
+ if body.Associate {
+ evURI, checked, cErr := checkin.Current(r.Context(), h.DB, viewerDID)
+ if cErr != nil {
+ slog.Warn("set connection event: checkin.Current", "err", cErr)
+ http.Error(w, "failed to resolve current event", http.StatusInternalServerError)
+ return
+ }
+ if !checked || evURI == "" {
+ http.Error(w, "not checked into an ongoing event", http.StatusBadRequest)
+ return
+ }
+ eventURI = evURI
+ }
+
+ // Find the most-recent connection record for this target.
+ viewerPDS := h.lookupPDSForDID(r, viewerDID)
+ entries, err := connection.List(r.Context(), viewerPDS, viewerDID)
+ if err != nil {
+ slog.Warn("set connection event: list connections", "err", err)
+ http.Error(w, "failed to load connection", http.StatusInternalServerError)
+ return
+ }
+ rec, found := connection.FindForTarget(entries, target)
+ if !found {
+ http.Error(w, "no connection record found", http.StatusNotFound)
+ return
+ }
+
+ if err := connection.SetEvent(r.Context(), sess, rec.URI, rec.With, rec.ConnectedAt, eventURI); err != nil {
+ slog.Warn("set connection event: putRecord", "uri", rec.URI, "err", err)
+ http.Error(w, "failed to update record", http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
+}
+```
+
+All referenced packages (`url`, `json`, `slog`, `http`, `chi`, `syntax`, `checkin`, `connection`) are already imported in this file.
+
+- [ ] **Step 2: Register the route**
+
+In `features/connections/routes.go`, add inside `SetupRoutes` after the notes route:
+
+```go
+ router.Post("/connections/{did}/event", h.SetConnectionEvent)
+```
+
+Update the doc comment above `SetupRoutes` to list the new route:
+
+```go
+// - POST /connections/{did}/event — associate/clear the current event tag
+```
+
+- [ ] **Step 3: Verify it builds**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go build ./...`
+Expected: silent success.
+
+- [ ] **Step 4: Commit**
+
+```bash
+cd /Users/brittany/Documents/Collective/atmoquest
+git add features/connections/handlers.go features/connections/routes.go
+git commit -m "feat(connections): POST /connections/{did}/event endpoint"
+```
+
+---
+
+### Task 5: Client script
+
+**Files:**
+- Create: `web/resources/static/js/connection-event.js`
+
+- [ ] **Step 1: Write the script**
+
+Create `web/resources/static/js/connection-event.js`:
+
+```javascript
+// atmo.quest — toggle whether a connection is tagged with the event you're
+// currently checked into. POSTs {associate: bool} to
+// /connections/{did}/event. The server re-derives which event; we only send
+// the boolean. On failure we revert the checkbox so the UI matches the PDS.
+(function () {
+ "use strict";
+
+ var toggle = document.querySelector(".pv-event-toggle");
+ if (!toggle) return;
+
+ var targetDID = toggle.getAttribute("data-target-did");
+ var checkbox = document.getElementById("pv-event-associate");
+ var status = document.getElementById("pv-event-status");
+ if (!targetDID || !checkbox) return;
+
+ function showStatus(msg) {
+ if (!status) return;
+ status.textContent = msg;
+ clearTimeout(status._timer);
+ status._timer = setTimeout(function () { status.textContent = ""; }, 2000);
+ }
+
+ checkbox.addEventListener("change", function () {
+ var desired = checkbox.checked;
+ checkbox.disabled = true;
+ fetch("/connections/" + encodeURI(targetDID) + "/event", {
+ method: "POST",
+ credentials: "same-origin",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ associate: desired })
+ })
+ .then(function (resp) {
+ if (resp.ok) {
+ showStatus("✓ saved");
+ } else {
+ checkbox.checked = !desired; // revert
+ showStatus("save failed");
+ }
+ })
+ .catch(function () {
+ checkbox.checked = !desired; // revert
+ showStatus("save failed");
+ })
+ .finally(function () {
+ checkbox.disabled = false;
+ });
+ });
+})();
+```
+
+- [ ] **Step 2: Confirm the file is syntactically valid**
+
+Run: `node --check /Users/brittany/Documents/Collective/atmoquest/web/resources/static/js/connection-event.js`
+Expected: silent success (exit 0).
+
+- [ ] **Step 3: Commit**
+
+```bash
+cd /Users/brittany/Documents/Collective/atmoquest
+git add web/resources/static/js/connection-event.js
+git commit -m "feat(connections): client toggle for current-event association"
+```
+
+---
+
+### Task 6: Full build, tests, and manual verification
+
+**Files:** none (verification only)
+
+- [ ] **Step 1: Build, vet, and test everything**
+
+Run: `cd /Users/brittany/Documents/Collective/atmoquest && go tool templ generate && go build ./... && go vet ./... && go test ./...`
+Expected: templ clean, build silent, vet silent, all tests `ok`.
+
+- [ ] **Step 2: Manual verification (requires a running instance + checked-in account)**
+
+1. Start the app: `go tool task live` and sign in as an ATProto user.
+2. Check into an ongoing event (so `checkin.Current` returns it).
+3. Open `/connections/{did}` for a person you have a `quest.atmo.connection` record with. Confirm the `📍 met at ` checkbox appears.
+4. Check the box → confirm `✓ saved`. Verify on the PDS:
+ `com.atproto.repo.getRecord` for that record now shows the `event` field set to the current event's at-uri.
+5. Uncheck the box → confirm `✓ saved` and that the record's `event` field is gone.
+6. Sign out / use a local account → confirm the checkbox does **not** appear.
+7. Open a connection profile while **not** checked into any ongoing event → confirm the checkbox does **not** appear.
+
+- [ ] **Step 3: Final commit (if templ regeneration changed anything)**
+
+```bash
+cd /Users/brittany/Documents/Collective/atmoquest
+git add -A
+git commit -m "chore(connections): regenerate templ for current-event association" || echo "nothing to commit"
+```
+
+---
+
+## Notes for the implementer
+
+- **In-place update is intentional.** When a connection record is already tagged with a *different* event, checking the box overwrites that event with the current one. This was an explicit product decision (see the design doc).
+- **Server is the source of truth for the event.** The client only sends `{associate: bool}`; the handler re-derives the event from `checkin.Current`. Never accept an event URI from the client.
+- **Scope:** local accounts and local-only connections are out of scope — they have no PDS record, so the checkbox is never rendered and the endpoint will 404 for them.
+- **Spec:** `docs/superpowers/specs/2026-06-01-associate-connection-with-current-event-design.md`.