diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 5d68fd6b..ccf4aed3 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -1235,10 +1235,20 @@ Plans:
3. Userfriends is checked against fonoteka first, so there is one shared copy.
4. The new code has unit tests, delivered in the phase's last plan.
-**Plans:** 0 plans
+**Plans:** 6 plans
Plans:
-- [ ] TBD (run $gsd-plan-phase 19.1 to break down)
+**Wave 1**
+- [ ] 19.1-01-PLAN.md — userfriends repo, baseline, friends HTTP tracer, group events, soft chat adapter, GDPR
+- [ ] 19.1-03-PLAN.md — chatbots repo, bot_flags, slash-command dispatcher, /ping tracer (no AI-plugin import)
+- [ ] 19.1-04-PLAN.md — webpush-go note, then herald subscriptions, PublicOnly send, rollback-sends-0 tracer
+
+**Wave 2** *(D-17 gate for forum; chatvideo after chat)*
+- [ ] 19.1-02-PLAN.md — forum repo after D-17 Render/Purify, DeleteContext, GFM/XSS HTTP tracer
+- [ ] 19.1-05-PLAN.md — LiveKit v2 note, then chatvideo sessions, authorizer, generic reconciler
+
+**Wave 3** *(blocked on Waves 1–2)*
+- [ ] 19.1-06-PLAN.md — PHP method maps, goldens, scripts/check-phase19.1.sh
### Phase 19.2: Chat polish against WavePath clients (INSERTED)
diff --git a/.planning/phases/19.1-chat-satellites/19.1-01-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-01-PLAN.md
new file mode 100644
index 00000000..8c211393
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/19.1-01-PLAN.md
@@ -0,0 +1,312 @@
+---
+phase: 19.1-chat-satellites
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - .planning/ROADMAP.md
+ - ../sm-userfriends-plugin/.gitignore
+ - ../sm-userfriends-plugin/go.mod
+ - ../sm-userfriends-plugin/go.sum
+ - ../sm-userfriends-plugin/go.work
+ - ../sm-userfriends-plugin/README.md
+ - ../sm-userfriends-plugin/plugin.go
+ - ../sm-userfriends-plugin/boot.go
+ - ../sm-userfriends-plugin/routes.go
+ - ../sm-userfriends-plugin/config/config.yaml
+ - ../sm-userfriends-plugin/lang/en/lang.yaml
+ - ../sm-userfriends-plugin/lang/pl/lang.yaml
+ - ../sm-userfriends-plugin/models/context.go
+ - ../sm-userfriends-plugin/models/friendship.go
+ - ../sm-userfriends-plugin/models/group.go
+ - ../sm-userfriends-plugin/models/group_member.go
+ - ../sm-userfriends-plugin/models/group_invite.go
+ - ../sm-userfriends-plugin/models/group_ban.go
+ - ../sm-userfriends-plugin/models/registry.go
+ - ../sm-userfriends-plugin/events/events.go
+ - ../sm-userfriends-plugin/classes/friendship_service.go
+ - ../sm-userfriends-plugin/classes/group_service.go
+ - ../sm-userfriends-plugin/classes/group_policy.go
+ - ../sm-userfriends-plugin/classes/group_visibility_gate.go
+ - ../sm-userfriends-plugin/classes/friends_context_adapter.go
+ - ../sm-userfriends-plugin/api/auth.go
+ - ../sm-userfriends-plugin/api/errors.go
+ - ../sm-userfriends-plugin/api/json.go
+ - ../sm-userfriends-plugin/api/friends.go
+ - ../sm-userfriends-plugin/api/groups.go
+ - ../sm-userfriends-plugin/updates/registry.go
+ - ../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go
+ - ../sm-userfriends-plugin/updates/baseline_test.go
+ - ../sm-userfriends-plugin/testmain_test.go
+ - ../sm-userfriends-plugin/harness_test.go
+ - ../sm-userfriends-plugin/fixture_plugin_test.go
+ - ../sm-userfriends-plugin/http_tracer_test.go
+autonomous: false
+requirements: [SC-19.1-1, SC-19.1-3, D-01, D-02, D-03, D-04, D-05, D-07, D-08]
+estimate:
+ tokens: 200000
+ raw_tokens: 200000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "D-01: module git.golem15.com/golem15/sm-userfriends-plugin, package userfriends, plugin ID golem15.userfriends, Requires golem15.user only; frozen PHP userfriends 182b514."
+ - "D-02 / D-04: one gormigrate ID 202610180001_create_golem15_userfriends_baseline uses CREATE TABLE IF NOT EXISTS / CREATE INDEX IF NOT EXISTS for golem15_userfriends_contexts, friendships, groups, group_members, group_invites, group_bans; demo-seed v1.0.1 ID is a comment and inserts no demo rows."
+ - "D-05: prefix /_userfriends/api/v1; JWT A request to B, accept, GET /friends returns 200; POST /friends/request uses bucket community-join at 10/min; actor id from the token."
+ - "D-08: events.GroupCreatedEventName userfriends.GroupCreated (and GroupDeleted, MemberJoined, MemberLeft); soft chat adapter via Lookup of chat Registry; Requires() never lists golem15.chat."
+ - "D-03: Boot Listens user.Deleting; heir is oldest admin else oldest member else hard-delete empty group; then member and ban rows go; order by role, not MySQL FIELD()."
+ - "SC-19.1-3: ROADMAP Phase 19.1 Repos names summercms.go; criterion 2 names webpush-go / LiveKit / bluemonday notes and no longer wires chatbots to the AI plugin; criterion 3 is marked already met."
+ artifacts:
+ - path: "../sm-userfriends-plugin/plugin.go"
+ provides: "golem15.userfriends plugin: Requires user only, config, lang, migrations, models, routes, buckets"
+ contains: "golem15.userfriends"
+ - path: "../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go"
+ provides: "idempotent userfriends v1.1.0 baseline"
+ contains: "IF NOT EXISTS"
+ - path: "../sm-userfriends-plugin/events/events.go"
+ provides: "group lifecycle festival events"
+ contains: "userfriends.GroupCreated"
+ - path: "../sm-userfriends-plugin/http_tracer_test.go"
+ provides: "JWT request/accept/list tracer and community-join bucket"
+ contains: "TestHTTPTracer"
+ - path: ".planning/ROADMAP.md"
+ provides: "Phase 19.1 Repos + SC2 + SC3 wording"
+ contains: "webpush-go"
+ key_links:
+ - from: "../sm-userfriends-plugin/boot.go"
+ to: "../sm-userfriends-plugin/events/events.go"
+ via: "Fire GroupCreated inside the write tx; Listen user.Deleting"
+ pattern: "GroupCreated"
+ - from: "../sm-userfriends-plugin/routes.go"
+ to: "../sm-userfriends-plugin/api/friends.go"
+ via: "jwt.auth plus throttle community-join on POST /friends/request"
+ pattern: "community-join"
+ - from: "../sm-userfriends-plugin/classes/friends_context_adapter.go"
+ to: "../sm-chat-plugin/contracts/registry.go"
+ via: "optional Lookup Registry; Add alias friendcontext"
+ pattern: "friendcontext"
+ prohibitions:
+ - statement: "Production sources, comments, lang and README never contain QuestStream, family, parent, child, or consuming-app names; plugin.name is Friends / Przyjaciele."
+ status: unresolved
+ verification: grep
+ - statement: "Requires() and go.mod never require sm-chat-plugin; sm-user-plugin is not edited."
+ status: unresolved
+ verification: grep
+ - statement: "Legacy /api/friends routes and Winter FriendsList / FriendRequests components are not registered (D-07)."
+ status: unresolved
+ verification: judgment
+---
+
+## Phase Goal
+
+**As a** application that mounts the satellites, **I want to** have Friends on SummerCMS with a generic `/_userfriends/api/v1`, **so that** a host can bind seams and call the published JSON API without PHP.
+
+This plan's slice (plan 1 of 6, wave 1): `golem15.userfriends` in a new repository, proven by JWT user A requesting B, accepting, and listing friends.
+
+## Spec-less probe fallback
+
+No SPEC.md and no REQUIREMENTS IDs exist for this phase: the orchestrator recorded "spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)". No probe predicates were generated. `must_haves` derive from ROADMAP success criteria SC-19.1-1..SC-19.1-4 and locked CONTEXT D-01..D-18.
+
+## Schema-gate recorded choice
+
+Schema-gate JS ORM push does not apply. This phase uses per-plugin gormigrate `CREATE TABLE IF NOT EXISTS` baselines (D-02). No prisma / drizzle / supabase / typeorm push task is added.
+
+## Decisions taken in this plan (Claude's discretion)
+
+- Package `userfriends`, PluginID `golem15.userfriends`, Winter directory layout. Friendship writes live in `classes.FriendshipService`; groups in `classes.GroupService`.
+- Festival names `userfriends.GroupCreated`, `userfriends.GroupDeleted`, `userfriends.MemberJoined`, `userfriends.MemberLeft` (one constant per line). Payload fields GroupID, ActorID, MemberID, Tx.
+- Soft chat adapter alias `friendcontext`, LegacyPHPClass `Golem15\UserFriends\Models\FriendContext`; optional group alias `friendgroup` / `Golem15\UserFriends\Models\FriendGroup` if the adapter registers groups.
+- APIPrefix `/_userfriends/api/v1`, APIBucket `userfriends-api` 120/min, JoinBucket `community-join` 10/min on friend request and group join.
+- Baseline id `202610180001_create_golem15_userfriends_baseline`.
+- First execute commit patches ROADMAP Phase 19.1 Repos + criterion 2 + criterion 3 (CONTEXT roadmap corrections).
+
+## Artifacts this phase produces (this plan)
+
+- New local repo `../sm-userfriends-plugin` (module `git.golem15.com/golem15/sm-userfriends-plugin`).
+- Types: `userfriends.Plugin`, `classes.FriendshipService`, `classes.GroupService`, `classes.GroupPolicy`, `classes.GroupVisibilityGate`, `events.GroupEvent`, models Context / Friendship / Group / GroupMember / GroupInvite / GroupBan.
+- Funcs: `FriendshipService.Request`, `Accept`, `List`; `GroupService.Create`; `Delete` cascade on user.Deleting; `Plugin.Routes`, `Plugin.Buckets`.
+- Config: none beyond plugin.name. Lang EN/PL plugin.name Friends / Przyjaciele.
+- Routes: `GET /_userfriends/api/v1/friends`, `GET .../friends/requests`, `POST .../friends/request`, `POST .../friends/requests/{id}/accept`, plus expansion reject/cancel/unfriend and `/groups*`.
+- Commands: none.
+
+
+Create sm-userfriends-plugin with the frozen v1.1.0 schema (D-01, D-02), friendship and group services that fire festival group events (D-08), a soft chat context adapter (D-08), the GDPR listener (D-03), and the generic friends API tracer (D-05).
+
+Purpose: later hosts mount `/_userfriends/api/v1` without rewriting controllers; the tracer proves request/accept/list before the remaining group matrix.
+Output: a compiling sibling module whose TestHTTPTracer sends a friend request as JWT A, accepts as JWT B, and lists 200.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/STATE.md
+@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md
+@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
+@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
+
+Concurrency: other sessions may edit summercms.go or STATE.md. Do not stage those. Do not edit modules/lighthouse, fetchguard, festival, conga, or ../fonoteka.go/plugins/golem15/user. Stage by explicit path. No co-author tags. Do not push unless the Task 1 reply says so.
+
+
+Analog (copy shape, change names):
+- ../sm-chat-plugin/plugin.go (PluginID, embed config/lang, Requires, init party.Register)
+- ../sm-chat-plugin/boot.go (registerBootStep / runBootSteps, Listen user.Deleting)
+- ../sm-chat-plugin/go.mod (sibling replace ../summercms.go)
+- ../sm-chat-plugin/go.work (user replace ../fonoteka.go/plugins/golem15/user)
+- ../sm-chat-plugin/routes.go (APIPrefix, Buckets, jwt.auth)
+- ../sm-chat-plugin/api/auth.go (frontendUser; actor from token)
+- ../sm-chat-plugin/api/errors.go (IDOR collapses to 404)
+- ../sm-chat-plugin/http_tracer_test.go (bootWeb, frontendToken, call)
+- ../sm-chat-plugin/events/events.go (one EventName per line, Tx field)
+- ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go (IF NOT EXISTS)
+- ../sm-chat-plugin/testmain_test.go (flag.Parse + testing.Short)
+- ../sm-chatchannels-plugin/plugin.go (HasRoutes, no hard extra require beyond PHP)
+- Frozen PHP at /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends @ 182b514
+- Host shapes at /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityFriendsController.php and CommunityGroupController.php (payloads only; do not port /_wave)
+
+
+
+
+
+
+ Task 1: Confirm D-01 names, D-02 baseline id and D-05 prefix before the new repo exists
+ .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md (D-01, D-02, D-05, D-08), .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (userfriends schema, Pitfall 11), .planning/notes/core-plugins-own-repos.md
+ Print a decision sheet, then stop. Do not create the directory, do not run tea, do not push. The sheet lists the locked identifiers this plan will write: module git.golem15.com/golem15/sm-userfriends-plugin, package userfriends, PluginID golem15.userfriends, tables golem15_userfriends_contexts / friendships / groups / group_members / group_invites / group_bans, migration id 202610180001_create_golem15_userfriends_baseline, import-compatible Postgres IF NOT EXISTS, published prefix /_userfriends/api/v1, community-join 10 per minute, Requires golem15.user only. Also list tea repos create --owner golem15 --name sm-userfriends-plugin (check tea repos create --help for flags) and that hold keeps work local. Confirm ROADMAP Phase 19.1 Repos still omits summercms.go and criterion 2 still names the AI plugin; Task 2 patches those two lines plus marks criterion 3 already met.
+ Lock D-01 plugin names, D-02 baseline id 202610180001_create_golem15_userfriends_baseline, and D-05 prefix /_userfriends/api/v1, then implement locally and optionally create the remote?
+ These names land in every adopting app's database and in go.mod require paths. Changing the migration id, table names, or published prefix after an app migrates needs a new migration and a client change. A pushed main is visible to every developer; no application mounts this plugin yet.
+
+
+
+
+
+ Reply proceed-and-push, proceed-local, or abort.
+
+ test -f .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md && grep -q "D-04" .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md && echo decision-sheet-ready
+ non-zero exit or "decision-sheet-ready" absent
+
+
+ - SUMMARY records the reply verbatim.
+ - After abort, ../sm-userfriends-plugin does not exist.
+
+ The human confirmed the locked names and the remote choice before any repo file was written.
+
+
+
+ Task 2: End-to-end friend request — repo, baseline, FriendshipService, JWT A requests B, accept, list 200
+ D-02 migration id and table shapes and D-05 published /_userfriends/api/v1 become the shared contract; undoing them needs a new migration and a client change.
+ Task 1 reply is proceed-and-push or proceed-local; ../sm-userfriends-plugin does not already exist as a foreign module.
+ ../sm-userfriends-plugin/go.mod, ../sm-userfriends-plugin/go.work, ../sm-userfriends-plugin/go.sum, ../sm-userfriends-plugin/.gitignore, ../sm-userfriends-plugin/README.md, ../sm-userfriends-plugin/plugin.go, ../sm-userfriends-plugin/boot.go, ../sm-userfriends-plugin/routes.go, ../sm-userfriends-plugin/config/config.yaml, ../sm-userfriends-plugin/lang/en/lang.yaml, ../sm-userfriends-plugin/lang/pl/lang.yaml, ../sm-userfriends-plugin/models/context.go, ../sm-userfriends-plugin/models/friendship.go, ../sm-userfriends-plugin/models/registry.go, ../sm-userfriends-plugin/classes/friendship_service.go, ../sm-userfriends-plugin/api/auth.go, ../sm-userfriends-plugin/api/errors.go, ../sm-userfriends-plugin/api/json.go, ../sm-userfriends-plugin/api/friends.go, ../sm-userfriends-plugin/updates/registry.go, ../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go, ../sm-userfriends-plugin/updates/baseline_test.go, ../sm-userfriends-plugin/testmain_test.go, ../sm-userfriends-plugin/harness_test.go, ../sm-userfriends-plugin/fixture_plugin_test.go, ../sm-userfriends-plugin/http_tracer_test.go, .planning/ROADMAP.md
+ ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/go.mod, ../sm-chat-plugin/go.work, ../sm-chat-plugin/routes.go, ../sm-chat-plugin/api/auth.go, ../sm-chat-plugin/api/errors.go, ../sm-chat-plugin/http_tracer_test.go, ../sm-chat-plugin/updates/202610160001_create_golem15_chat_baseline.go, ../sm-chat-plugin/testmain_test.go, ../sm-chat-plugin/harness_test.go, ../sm-chat-plugin/fixture_plugin_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/updates/, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/models/Friendship.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/controllers/api/FriendsApiController.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityFriendsController.php, .planning/ROADMAP.md
+ Per D-01 and D-02, create ../sm-userfriends-plugin as a sibling of summercms.go. Copy the analog go.mod pin set (Go 1.27.0, gorm v1.31.2, gormigrate v2.1.7, pgx v5.10.0, testcontainers v0.44.0, summercms + sm-user-plugin require). replace summercms => ../summercms.go. go.work also replaces sm-user-plugin => ../fonoteka.go/plugins/golem15/user. Do not copy a deep relative replace. The production module list matches analog chat except it does not pull the chat module. No new third-party library.
+
+plugin.go: package userfriends, PluginID golem15.userfriends, Requires golem15.user only, embed config and lang, assert party.Plugin plus pact.HasConfig, HasMigrations, HasModels, HasLang, HasRoutes, surf.BucketProvider. README H1 Friends, one-sentence summary, import line, **Name:** mnemonic, standard sections; names only acme / blog; plugin display name Friends / Przyjaciele.
+
+In the same first execute commit, patch .planning/ROADMAP.md Phase 19.1 only: Repos add summercms.go for the Markdown module; criterion 2 drop the clause that wires chatbots to the AI plugin and name webpush-go / LiveKit / bluemonday notes; criterion 3 mark already met (fonoteka PHP tree equals Wavepath 182b514). Do not rewrite other phases.
+
+Baseline 202610180001_create_golem15_userfriends_baseline (D-02): CREATE TABLE IF NOT EXISTS for the six PHP v1.1.0 tables and their indexes from RESEARCH Frozen inventory. Types TIMESTAMPTZ. Status columns varchar + CHECK matching pending|accepted|declined and owner|admin|member and banned. Insert the FriendContext singleton id=1. Comment the demo-seed v1.0.1 version id; do not INSERT demo users (D-07). updates/registry.go copies analog Register / All / run. updates/baseline_test.go greps the six table names under -short.
+
+Models (leaf): Friendship.TableName golem15_userfriends_friendships; Context.TableName golem15_userfriends_contexts; models/registry.go Register / All. No festival imports in models/.
+
+classes.FriendshipService: Options{DB, Bus}, New refuses nil DB. This tracer implements Request(actorID, recipientID), Accept(actorID, friendshipID), List(actorID) matching CommunityFriendsController sendRequest / accept / index. Persist inside lagoon.Transaction. Sentinel errors prefixed golem15.userfriends:. Actor id is the argument, never a body field (D-05).
+
+Routes (D-05): APIPrefix /_userfriends/api/v1. Group jwt.auth + throttle:userfriends-api for GET /friends, GET /friends/requests, POST /friends/requests/{id}/accept. Separate group jwt.auth + throttle:community-join for POST /friends/request only. Buckets: userfriends-api Max 120 Decay time.Minute; community-join Max 10 Decay time.Minute; key frontend user id else IP, analog chat Buckets. api/auth.go copies frontendUser; missing JWT 401 Authentication required. api/errors.go: missing / unauthorized collapse to 404 Not found.
+
+Test host (D-04 smoke): testmain_test.go flag.Parse then testing.Short; harness boots golem15.user + golem15.userfriends with SUMMER_REALTIME__DRIVER=memory; fixturePluginID acme.teams; TestFixtureAbsentFromProduction greps production sources for the fixture id and the retired tokens. http_tracer_test.go TestHTTPTracer (Postgres, skip under -short): two frontend users, A POST /friends/request {user_id: B}, B POST accept, A GET /friends 200 with B. Also assert community-join Max is 10. TestBaseline runs under -short.
+
+Task 3 adds groups, festival events, the soft chat adapter, GDPR, and the remaining friend routes. Do not register legacy /api/friends paths or Winter components (D-07).
+
+ go -C ../sm-userfriends-plugin vet ./... && go -C ../sm-userfriends-plugin test -count=1 -short ./...
+ non-zero exit, or "ok" lines absent, or a package reports FAIL
+ docker info >/dev/null 2>&1 && go -C ../sm-userfriends-plugin test -count=1 -run '^TestHTTPTracer$' -v .
+ non-zero exit: no Docker daemon (record NOT RUN in the SUMMARY), or TestHTTPTracer is missing, fails, or accept/list is not 200
+
+
+ - grep -v '^#' ../sm-userfriends-plugin/plugin.go | grep -c 'golem15.userfriends' prints at least 1.
+ - grep -v '^#' ../sm-userfriends-plugin/updates/202610180001_create_golem15_userfriends_baseline.go | grep -c 'golem15_userfriends_friendships' prints at least 1.
+ - grep -n 'community-join' ../sm-userfriends-plugin/routes.go prints at least one line.
+ - grep -n 'Max: 10' ../sm-userfriends-plugin/routes.go prints at least one line.
+ - grep -n 'func (p \*Plugin) Requires' -A2 ../sm-userfriends-plugin/plugin.go | grep -c 'golem15.chat' prints 0.
+ - grep -n 'webpush-go' .planning/ROADMAP.md prints at least one line inside the Phase 19.1 section.
+
+ JWT A can request B, B can accept, A can list friends 200; the baseline and ROADMAP corrections are committed.
+
+
+
+ Task 3: Expand groups, festival events, soft chat adapter, GDPR heir, remaining friend routes
+ D-01 table and event names are copied into every adopting app; D-08 group events become the host wiring contract.
+ ../sm-userfriends-plugin/models/group.go, ../sm-userfriends-plugin/models/group_member.go, ../sm-userfriends-plugin/models/group_invite.go, ../sm-userfriends-plugin/models/group_ban.go, ../sm-userfriends-plugin/events/events.go, ../sm-userfriends-plugin/classes/group_service.go, ../sm-userfriends-plugin/classes/group_policy.go, ../sm-userfriends-plugin/classes/group_visibility_gate.go, ../sm-userfriends-plugin/classes/friends_context_adapter.go, ../sm-userfriends-plugin/api/friends.go, ../sm-userfriends-plugin/api/groups.go, ../sm-userfriends-plugin/routes.go, ../sm-userfriends-plugin/boot.go, ../sm-userfriends-plugin/plugin.go, ../sm-userfriends-plugin/README.md
+ ../sm-userfriends-plugin/classes/friendship_service.go, ../sm-chat-plugin/events/events.go, ../sm-chat-plugin/boot.go, ../sm-chat-plugin/contracts/registry.go, ../sm-chatchannels-plugin/classes/channel_service.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/GroupPolicy.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/GroupVisibilityGate.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/services/GroupService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/services/GroupMembershipService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/userfriends/classes/FriendsContextAdapter.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityFriendsController.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityGroupController.php
+ Per D-08, add events with GroupCreatedEventName userfriends.GroupCreated, GroupDeletedEventName userfriends.GroupDeleted, MemberJoinedEventName userfriends.MemberJoined, MemberLeftEventName userfriends.MemberLeft (one constant per line). GroupEvent fields GroupID, ActorID, MemberID, Tx. Fire inside lagoon.Transaction after the row is saved.
+
+Port GroupPolicy and GroupVisibilityGate 1:1 from PHP (matrix tests land in plan 06). GroupService.Create / Delete / Update and membership join / leave / kick / ban / unban / promote / demote / transfer fire the matching event. community-join 10/min also covers POST /groups/{id}/join when that route is mounted.
+
+Remaining friend routes matching CommunityFriendsController: GET /friends/sent, DELETE /friends/requests/{id} reject, DELETE /friends/sent/{id} cancel, DELETE /friends/{userId} unfriend. Group routes under /_userfriends/api/v1/groups matching D-05 (groups, members, invites, bans). Do not add webhook, emoji, chat-message, or image-upload host routes.
+
+Soft chat adapter (D-08): in Boot, Lookup the chat Registry type; if the lookup fails, skip. If it succeeds, Add PluginID with Registration Alias friendcontext and LegacyPHPClass the PHP FriendContext class string. Requires and go.mod stay limited to the user plugin; chat is optional at Boot only.
+
+GDPR (D-03): app.Events.Listen the user Deleting event; WithDB(ev.Tx); heir oldest admin else oldest member else hard-delete the empty group; then delete that user's member and ban rows. Order by role, not a MySQL FIELD() expression.
+
+If Task 1 replied proceed-and-push: after verify is green, tea repos create --owner golem15 --name sm-userfriends-plugin, add origin, git push -u origin main (never force). If proceed-local: no remote. Record the reply outcome in the SUMMARY. Do not edit the user plugin.
+
+ go -C ../sm-userfriends-plugin vet ./... && go -C ../sm-userfriends-plugin test -count=1 -short ./...
+ non-zero exit or a package reports FAIL
+
+
+ - grep -n 'userfriends.GroupCreated' ../sm-userfriends-plugin/events/events.go prints one line.
+ - grep -n 'userfriends.MemberLeft' ../sm-userfriends-plugin/events/events.go prints one line.
+ - grep -n 'friendcontext' ../sm-userfriends-plugin/boot.go ../sm-userfriends-plugin/classes/friends_context_adapter.go prints at least one line.
+ - grep -n 'DeletingEvent' ../sm-userfriends-plugin/boot.go prints at least one line.
+ - grep -n 'func (p \*Plugin) Requires' -A3 ../sm-userfriends-plugin/plugin.go | grep -c 'golem15.chat' prints 0.
+
+ Groups fire festival events, chat is optional, and deleting a user hands off or removes their groups.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| JWT actor → /_userfriends/api/v1 | untrusted body and path ids cross here |
+| FriendshipService write → Postgres | actor ids become friendship and group rows |
+| user.Deleting → GDPR cascade | a deleted user must not leave owned groups orphaned |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-19.1-01 | Information disclosure | api/friends.go | high | mitigate | Actor from token; stranger accept/list/unfriend collapse to 404; community-join 10/min on request |
+| T-19.1-08 | Information disclosure | boot.go onUserDeleting | medium | mitigate | Heir or hard-delete then remove member and ban rows on user.Deleting |
+| T-19.1-SC | Tampering | go.mod requires | high | mitigate | Analog pins only; no new registry packages |
+
+
+
+
+go -C ../sm-userfriends-plugin vet ./... and go -C ../sm-userfriends-plugin test -count=1 -short ./... stay green. TestHTTPTracer on Docker proves request/accept/list. ROADMAP Phase 19.1 Repos, criterion 2 and criterion 3 match CONTEXT.
+
+
+
+sm-userfriends-plugin exists with the v1.1.0 baseline, a working friends HTTP tracer, group festival events, a soft chat adapter, and the GDPR listener. summercms.go modules are untouched aside from the ROADMAP wording patch.
+
+
+
diff --git a/.planning/phases/19.1-chat-satellites/19.1-02-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-02-PLAN.md
new file mode 100644
index 00000000..62da8234
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/19.1-02-PLAN.md
@@ -0,0 +1,272 @@
+---
+phase: 19.1-chat-satellites
+plan: 02
+type: execute
+wave: 2
+depends_on: []
+files_modified:
+ - .planning/notes/2026-10-18-bluemonday-forum-policy.md
+ - ../sm-forum-plugin/.gitignore
+ - ../sm-forum-plugin/go.mod
+ - ../sm-forum-plugin/go.sum
+ - ../sm-forum-plugin/go.work
+ - ../sm-forum-plugin/README.md
+ - ../sm-forum-plugin/plugin.go
+ - ../sm-forum-plugin/boot.go
+ - ../sm-forum-plugin/routes.go
+ - ../sm-forum-plugin/config/config.yaml
+ - ../sm-forum-plugin/lang/en/lang.yaml
+ - ../sm-forum-plugin/lang/pl/lang.yaml
+ - ../sm-forum-plugin/contracts/access_resolver.go
+ - ../sm-forum-plugin/classes/deny_all.go
+ - ../sm-forum-plugin/classes/forum_service.go
+ - ../sm-forum-plugin/models/category.go
+ - ../sm-forum-plugin/models/thread.go
+ - ../sm-forum-plugin/models/post.go
+ - ../sm-forum-plugin/models/thread_read.go
+ - ../sm-forum-plugin/models/registry.go
+ - ../sm-forum-plugin/api/auth.go
+ - ../sm-forum-plugin/api/errors.go
+ - ../sm-forum-plugin/api/json.go
+ - ../sm-forum-plugin/api/categories.go
+ - ../sm-forum-plugin/api/threads.go
+ - ../sm-forum-plugin/api/posts.go
+ - ../sm-forum-plugin/console/rewrite_context_types.go
+ - ../sm-forum-plugin/updates/registry.go
+ - ../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go
+ - ../sm-forum-plugin/updates/baseline_test.go
+ - ../sm-forum-plugin/testmain_test.go
+ - ../sm-forum-plugin/harness_test.go
+ - ../sm-forum-plugin/fixture_plugin_test.go
+ - ../sm-forum-plugin/http_tracer_test.go
+autonomous: true
+requirements: [SC-19.1-1, D-01, D-02, D-03, D-04, D-05, D-08, D-16, D-17, D-18]
+estimate:
+ tokens: 200000
+ raw_tokens: 200000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "D-17: forum execution starts only after a summercms.go module exports Render(markdown) and Purify(html); sm-forum-plugin go.mod never requires bluemonday or goldmark."
+ - "D-16: the bluemonday forum-policy note exists (this plan writes it unless D-17 already did) and names v1.0.27, AllowElements of the PHP list, schemes http/https/mailto, RequireNoFollowOnLinks true, AddTargetBlankToFullyQualifiedLinks true, and is not the richeditor UGC policy."
+ - "D-02: baseline 202610180001_create_golem15_forum_baseline creates golem15_forum_categories (context_type NOT NULL, no FK), threads, posts, thread_reads with IF NOT EXISTS."
+ - "D-05 / D-18: /_forum/api/v1/contexts/{type}/{id}/categories tracer creates category+thread+post; content_html has a table for GFM and no script element; adopted content_html is served stored."
+ - "D-03 / D-08: ForumAccessResolver default DenyAll with Boot Warn and SetAccessResolver; DeleteContext(type, id) soft-deletes the morph pair; forum never imports userfriends; forum:rewrite-context-types owns golem15_forum_categories.context_type."
+ artifacts:
+ - path: "../sm-forum-plugin/plugin.go"
+ provides: "golem15.forum plugin, Requires golem15.user"
+ contains: "golem15.forum"
+ - path: "../sm-forum-plugin/classes/forum_service.go"
+ provides: "ForumService including DeleteContext"
+ contains: "DeleteContext"
+ - path: "../sm-forum-plugin/contracts/access_resolver.go"
+ provides: "ForumAccessResolver seam"
+ contains: "CanRead"
+ - path: "../sm-forum-plugin/console/rewrite_context_types.go"
+ provides: "forum:rewrite-context-types"
+ contains: "forum:rewrite-context-types"
+ - path: ".planning/notes/2026-10-18-bluemonday-forum-policy.md"
+ provides: "D-16 named forum policy note (skipped if D-17 already wrote one)"
+ contains: "bluemonday"
+ key_links:
+ - from: "../sm-forum-plugin/classes/forum_service.go"
+ to: "D-17 Render/Purify"
+ via: "write-path render of new and edited posts"
+ pattern: "Render\\("
+ - from: "../sm-forum-plugin/boot.go"
+ to: "../sm-forum-plugin/classes/deny_all.go"
+ via: "Boot Warn while DenyAll is the resolver"
+ pattern: "deny-all"
+ - from: "../sm-forum-plugin/console/rewrite_context_types.go"
+ to: "../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go"
+ via: "rewrites golem15_forum_categories.context_type LegacyPHPClass to alias"
+ pattern: "context_type"
+ prohibitions:
+ - statement: "forum does not vendor goldmark or bluemonday and does not import the richeditor UGC policy or cabana.RenderMarkdown."
+ status: unresolved
+ verification: grep
+ - statement: "forum go.mod never requires sm-userfriends-plugin."
+ status: unresolved
+ verification: grep
+ - statement: "Adopted content_html is never re-rendered on read (D-18)."
+ status: unresolved
+ verification: judgment
+---
+
+## Phase Goal
+
+**As a** application that mounts the satellites, **I want to** have Forum on SummerCMS with a generic `/_forum/api/v1`, **so that** a host can bind ForumAccessResolver and call context-scoped categories without PHP.
+
+This plan's slice (plan 2 of 6, wave 2): `golem15.forum` after D-17 `Render` / `Purify` exists.
+
+## Decisions taken in this plan (Claude's discretion)
+
+- Package `forum`, PluginID `golem15.forum`, Requires `golem15.user` only.
+- ForumAccessResolver methods CanRead / CanPost / CanManage / CanManageContext matching PHP DenyAllForumAccessResolver.
+- DeleteContext(ctx, type, id) is a new Go API (Pitfall 12). Morph rewrite is forum-owned `forum:rewrite-context-types`.
+- APIPrefix `/_forum/api/v1`, APIBucket `forum-api` 120/min.
+- Baseline id `202610180001_create_golem15_forum_baseline`.
+- Bluemonday note path `.planning/notes/2026-10-18-bluemonday-forum-policy.md` unless D-17 already wrote a named-policy note.
+
+## Artifacts this phase produces (this plan)
+
+- `../sm-forum-plugin` module `git.golem15.com/golem15/sm-forum-plugin`.
+- Types: `forum.Plugin`, `contracts.AccessResolver`, `classes.DenyAll`, `classes.ForumService`.
+- Funcs: `ForumService.CreateCategory`, `CreateThread`, `Reply`, `MarkThreadRead`, `UnreadThreadCount`, `DeleteContext`, `RewriteContextTypes`.
+- Command: `summer forum:rewrite-context-types`.
+- Routes under `/_forum/api/v1/contexts/{type}/{id}/...`.
+
+
+Write the D-16 forum-policy note if missing, then create sm-forum-plugin that consumes D-17 Render/Purify (D-16, D-17, D-18), ships the v1.1.0 baseline (D-02), DenyAll ForumAccessResolver (D-03), DeleteContext (D-08), and the generic forum API tracer (D-05).
+
+Purpose: hosts bind the resolver; forum never imports userfriends.
+Output: TestHTTPTracer creates a GFM table post whose stored content_html has a table and no script element.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md
+@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
+@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
+@.planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md
+@.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md
+
+
+Analog:
+- ../sm-chatchannels-plugin/contracts/access_resolver.go
+- ../sm-chatchannels-plugin/classes/deny_all.go
+- ../sm-chatchannels-plugin/boot.go (deny-all Warn, SetAccessResolver)
+- ../sm-chatchannels-plugin/routes.go (contexts/{type}/{id})
+- ../sm-chat-plugin/console/rewrite_context_types.go
+- ../sm-chat-plugin/http_tracer_test.go
+- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum @ ad10b54
+- Host CommunityForumController for payload keys only
+
+
+
+
+
+
+ Task 1: D-16 forum-policy note and D-17 Render/Purify gate
+ .planning/notes/2026-10-18-bluemonday-forum-policy.md
+ .planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md, .planning/notes/2026-10-11-framework-html-sanitizer-bluemonday.md, .planning/phases/19.1-chat-satellites/19.1-CONTEXT.md (D-16, D-17), modules/seaglass/seaglass.go, modules/cabana/field_markdown.go, modules/cabana/field_markdown.go
+ Per D-16, if a note already exists under .planning/notes/ that names a dedicated forum policy (AllowElements of the PHP list, schemes http/https/mailto, nofollow on, target blank on) and is not the 2026-10-11 richeditor note, skip writing and record the path in the SUMMARY. Otherwise write .planning/notes/2026-10-18-bluemonday-forum-policy.md in the hashids-note shape: library pin github.com/microcosm-cc/bluemonday v1.0.27 already in framework go.mod; named forum policy (not the richeditor UGC policy); goldmark GFM already a framework dep; D-17 owns Render and Purify; forum must not vendor either library.
+
+Then assert D-17: a summercms.go package exports Render and Purify (grep modules/ for `func Render(` and `func Purify(` outside cabana.RenderMarkdown). If they are absent, STOP — do not create ../sm-forum-plugin and do not grow a local renderer. This is the D-17 gate (Pitfall 6).
+
+ test -f .planning/notes/2026-10-18-bluemonday-forum-policy.md -o -n "$(grep -l 'RequireNoFollowOnLinks' .planning/notes/*bluemonday* 2>/dev/null | grep -v framework-html-sanitizer)" && echo forum-policy-note-ready
+ "forum-policy-note-ready" absent
+ rg -n 'func Render\(|func Purify\(' modules --glob '*.go' | grep -v 'RenderMarkdown' | grep -q 'func Render' && rg -n 'func Purify\(' modules --glob '*.go' | grep -q 'func Purify' && echo d17-ready
+ "d17-ready" absent — D-17 module missing; halt, do not start Task 2
+
+
+ - A .planning/notes/ file names bluemonday v1.0.27 and the PHP allow-list.
+ - grep -n 'func Render(' modules --glob '*.go' | grep -v RenderMarkdown prints at least one line.
+ - grep -n 'func Purify(' modules --glob '*.go' prints at least one line.
+
+ The forum-policy note exists and D-17 Render/Purify are importable.
+
+
+
+ Task 2: End-to-end forum post — category, thread, GFM table, no script element
+ D-02 forum baseline id and D-05 /_forum/api/v1 become the shared contract.
+ D-17 exports Render and Purify (Task 1 printed d17-ready). ../sm-forum-plugin does not already exist as a foreign module.
+ ../sm-forum-plugin/go.mod, ../sm-forum-plugin/go.work, ../sm-forum-plugin/go.sum, ../sm-forum-plugin/.gitignore, ../sm-forum-plugin/README.md, ../sm-forum-plugin/plugin.go, ../sm-forum-plugin/boot.go, ../sm-forum-plugin/routes.go, ../sm-forum-plugin/config/config.yaml, ../sm-forum-plugin/lang/en/lang.yaml, ../sm-forum-plugin/lang/pl/lang.yaml, ../sm-forum-plugin/contracts/access_resolver.go, ../sm-forum-plugin/classes/deny_all.go, ../sm-forum-plugin/classes/forum_service.go, ../sm-forum-plugin/models/category.go, ../sm-forum-plugin/models/thread.go, ../sm-forum-plugin/models/post.go, ../sm-forum-plugin/models/thread_read.go, ../sm-forum-plugin/models/registry.go, ../sm-forum-plugin/api/auth.go, ../sm-forum-plugin/api/errors.go, ../sm-forum-plugin/api/json.go, ../sm-forum-plugin/api/categories.go, ../sm-forum-plugin/api/threads.go, ../sm-forum-plugin/api/posts.go, ../sm-forum-plugin/updates/registry.go, ../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go, ../sm-forum-plugin/updates/baseline_test.go, ../sm-forum-plugin/testmain_test.go, ../sm-forum-plugin/harness_test.go, ../sm-forum-plugin/fixture_plugin_test.go, ../sm-forum-plugin/http_tracer_test.go
+ ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/contracts/access_resolver.go, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chatchannels-plugin/boot.go, ../sm-chatchannels-plugin/routes.go, ../sm-chat-plugin/go.mod, ../sm-chat-plugin/http_tracer_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/ForumService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/DenyAllForumAccessResolver.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/updates/, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/tests/security/ForumMarkdownRenderTest.php
+ Per D-01 and D-02, create ../sm-forum-plugin as a sibling. Analog go.mod pins; replace summercms => ../summercms.go; go.work replaces sm-user-plugin. go.mod stays on analog pins plus the user plugin. Consume D-17 from summercms; do not add markdown libraries or the friends module.
+
+plugin.go: package forum, PluginID golem15.forum, Requires golem15.user, HasRoutes, BucketProvider, HasCommands (rewrite lands in Task 3). README H1 Forum; plugin.name Forum / Forum. SetAccessResolver analog chatchannels.
+
+Baseline 202610180001_create_golem15_forum_baseline: four PHP v1.1.0 tables from RESEARCH. categories.context_type NOT NULL, no FK; unique (context_type, context_id, slug); threads.user_id and posts.user_id nullable SET NULL; deletion_reason varchar check self|moderator or null. TestBaseline greps the four table names.
+
+contracts.AccessResolver: CanRead, CanPost, CanManage, CanManageContext. classes.DenyAll all false. ForumService.New installs DenyAll when Resolver is nil; SetAccessResolver(nil) becomes DenyAll. Boot Warn while the default is active (D-03).
+
+ForumService write path (D-17, D-18): CreateCategory, CreateThread, Reply persist inside lagoon.Transaction. New and edited posts call framework Render on markdown and store content_html; never call the richeditor UGC sanitizer or cabana.RenderMarkdown. GET returns the stored content_html and does not call Render again.
+
+Routes (D-05): APIPrefix /_forum/api/v1, jwt.auth + throttle:forum-api 120/min. Paths /contexts/{type}/{id}/categories (GET, POST), POST .../categories/{categoryId}/threads, POST .../threads/{threadId}/posts, POST .../threads/{threadId}/read, GET .../unread. Actor from token.
+
+Test host: golem15.user + golem15.forum + acme.teams. After Boot the harness installs an allow-all test resolver so the tracer can write. TestAccessResolverDefault (short) asserts DenyAll before that swap. TestHTTPTracer (Docker): create category+thread+post whose body is a GFM table plus a script tag; stored content_html contains a table element and does not contain a script element; mark read; unread count 0. TestNoUserfriendsImport (short) greps production sources for the friends module path.
+
+Remote policy follows plan 01 Task 1: tea-create only on proceed-and-push; otherwise stay local. Never force-push.
+
+ go -C ../sm-forum-plugin vet ./... && go -C ../sm-forum-plugin test -count=1 -short ./...
+ non-zero exit, or "ok" lines absent, or a package reports FAIL
+ docker info >/dev/null 2>&1 && go -C ../sm-forum-plugin test -count=1 -run '^TestHTTPTracer$' -v .
+ non-zero exit: no Docker daemon (record NOT RUN), or TestHTTPTracer missing/fails, or content_html still contains a script element
+
+
+ - grep -v '^#' ../sm-forum-plugin/plugin.go | grep -c 'golem15.forum' prints at least 1.
+ - grep -v '^#' ../sm-forum-plugin/updates/202610180001_create_golem15_forum_baseline.go | grep -c 'golem15_forum_categories' prints at least 1.
+ - grep -n 'func (DenyAll)' ../sm-forum-plugin/classes/deny_all.go prints at least one line.
+ - grep -n 'sm-userfriends-plugin' ../sm-forum-plugin/go.mod prints 0 lines.
+ - grep -n 'bluemonday\|goldmark' ../sm-forum-plugin/go.mod prints 0 lines.
+
+ An allow-all test host can create a sanitized GFM post through /_forum/api/v1; deny-all remains the production default.
+
+
+
+ Task 3: DeleteContext, morph rewrite, remaining routes, GDPR null authorship
+ forum:rewrite-context-types changes stored context_type rows; PHP cannot resolve rewritten aliases.
+ ../sm-forum-plugin/classes/forum_service.go, ../sm-forum-plugin/console/rewrite_context_types.go, ../sm-forum-plugin/api/categories.go, ../sm-forum-plugin/api/threads.go, ../sm-forum-plugin/api/posts.go, ../sm-forum-plugin/routes.go, ../sm-forum-plugin/boot.go, ../sm-forum-plugin/plugin.go, ../sm-forum-plugin/README.md
+ ../sm-forum-plugin/classes/forum_service.go, ../sm-chat-plugin/console/rewrite_context_types.go, ../sm-chat-plugin/classes/writes.go, ../sm-chatchannels-plugin/boot.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/classes/ForumService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityForumController.php
+ Per D-08, add ForumService.DeleteContext(ctx, type, id): soft-delete categories (and cascaded threads/posts via existing FKs / deleted_at) for that morph pair. Do not import userfriends. The host wires group-deleted to this method later.
+
+Per D-03 morph rewrite: command forum:rewrite-context-types walks registered LegacyPHPClass values and UPDATE golem15_forum_categories SET context_type = alias WHERE context_type = that class, idempotent. Do not extend chat's command.
+
+Remaining ForumService methods from PHP: seedDefaultCategory, renameCategory, reorderCategories, deleteCategory, editPost, deletePost, deleteThread, unreadThreadCount, markThreadRead. HTTP: PUT/DELETE categories, reorder, GET threads/posts, PUT/DELETE posts, DELETE threads, preview. 404 collapse for unknown type / unauthorized.
+
+GDPR (D-03): Listen user.Deleting; set threads.user_id and posts.user_id to null; keep bodies.
+
+README documents DeleteContext, the rewrite command, and that the host must SetAccessResolver.
+
+ go -C ../sm-forum-plugin vet ./... && go -C ../sm-forum-plugin test -count=1 -short ./...
+ non-zero exit or a package reports FAIL
+
+
+ - grep -n 'func (s \*ForumService) DeleteContext' ../sm-forum-plugin/classes/forum_service.go prints one line.
+ - grep -n 'forum:rewrite-context-types' ../sm-forum-plugin/console/rewrite_context_types.go prints one line.
+ - grep -n 'DeletingEvent' ../sm-forum-plugin/boot.go prints at least one line.
+ - grep -n 'sm-userfriends-plugin' ../sm-forum-plugin/go.mod ../sm-forum-plugin/*.go ../sm-forum-plugin/classes/*.go prints 0 lines.
+
+ Forum can cascade a generic context delete, rewrite adopted class strings, and null authorship on user delete.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| JWT actor → /_forum/api/v1 | untrusted markdown and context ids |
+| ForumService write → content_html | sanitizer boundary (D-16/D-17) |
+| ForumAccessResolver | host-supplied allow; default deny-all |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-19.1-04 | Tampering | classes/forum_service.go write path | high | mitigate | D-17 Render/Purify on write; XSS corpus in TestHTTPTracer; adopted HTML served stored (D-18) |
+| T-19.1-10 | Elevation of privilege | classes/deny_all.go | high | mitigate | DenyAll default + Boot Warn + setter; IDOR → 404 |
+| T-19.1-SC | Tampering | go.mod requires | high | mitigate | No new libraries in forum; consume D-17 |
+
+
+
+
+Task 1 prints d17-ready before any forum file is written. go -C ../sm-forum-plugin test -short stays green. TestHTTPTracer proves GFM table and stripped script.
+
+
+
+sm-forum-plugin exists, consumes D-17, deny-all is the default, DeleteContext is generic, and the HTTP tracer stores sanitized content_html.
+
+
+
diff --git a/.planning/phases/19.1-chat-satellites/19.1-03-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-03-PLAN.md
new file mode 100644
index 00000000..8208f6f7
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/19.1-03-PLAN.md
@@ -0,0 +1,226 @@
+---
+phase: 19.1-chat-satellites
+plan: 03
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - ../sm-chatbots-plugin/.gitignore
+ - ../sm-chatbots-plugin/go.mod
+ - ../sm-chatbots-plugin/go.sum
+ - ../sm-chatbots-plugin/go.work
+ - ../sm-chatbots-plugin/README.md
+ - ../sm-chatbots-plugin/plugin.go
+ - ../sm-chatbots-plugin/boot.go
+ - ../sm-chatbots-plugin/routes.go
+ - ../sm-chatbots-plugin/config/config.yaml
+ - ../sm-chatbots-plugin/lang/en/lang.yaml
+ - ../sm-chatbots-plugin/lang/pl/lang.yaml
+ - ../sm-chatbots-plugin/contracts/slash_command.go
+ - ../sm-chatbots-plugin/contracts/bot_auth_resolver.go
+ - ../sm-chatbots-plugin/classes/deny_all.go
+ - ../sm-chatbots-plugin/classes/registry.go
+ - ../sm-chatbots-plugin/classes/dispatcher.go
+ - ../sm-chatbots-plugin/classes/help.go
+ - ../sm-chatbots-plugin/models/bot_flag.go
+ - ../sm-chatbots-plugin/models/registry.go
+ - ../sm-chatbots-plugin/api/auth.go
+ - ../sm-chatbots-plugin/api/errors.go
+ - ../sm-chatbots-plugin/api/json.go
+ - ../sm-chatbots-plugin/api/commands.go
+ - ../sm-chatbots-plugin/updates/registry.go
+ - ../sm-chatbots-plugin/updates/202610180001_create_golem15_chatbots_baseline.go
+ - ../sm-chatbots-plugin/updates/baseline_test.go
+ - ../sm-chatbots-plugin/testmain_test.go
+ - ../sm-chatbots-plugin/harness_test.go
+ - ../sm-chatbots-plugin/fixture_plugin_test.go
+ - ../sm-chatbots-plugin/http_tracer_test.go
+autonomous: true
+requirements: [SC-19.1-1, SC-19.1-2, D-01, D-02, D-03, D-04, D-05]
+estimate:
+ tokens: 160000
+ raw_tokens: 160000
+ tasks: 2
+ confidence: low
+must_haves:
+ truths:
+ - "D-01 / SC-19.1-2: module git.golem15.com/golem15/sm-chatbots-plugin, package chatbots, PluginID golem15.chatbots, Requires golem15.chat and golem15.user; go.mod never requires the AI plugin."
+ - "D-02: baseline 202610180001_create_golem15_chatbots_baseline creates golem15_chatbots_bot_flags with unique user_id."
+ - "D-05 / CMD-06: POST /_chatbots/api/v1/dispatch body {input} uses the JWT actor; register /ping returns ephemeral and inserts 0 chat messages; GET /commands lists /help after isAvailable."
+ - "D-03: BotAuthResolver default DenyAll with setter and Boot Warn; user.Deleting deletes the bot_flags row."
+ artifacts:
+ - path: "../sm-chatbots-plugin/plugin.go"
+ provides: "golem15.chatbots plugin"
+ contains: "golem15.chatbots"
+ - path: "../sm-chatbots-plugin/classes/registry.go"
+ provides: "SlashCommandRegistry with reserved help and duplicate rejection"
+ contains: "registerBuiltin"
+ - path: "../sm-chatbots-plugin/classes/dispatcher.go"
+ provides: "CommandDispatcher"
+ contains: "Dispatch"
+ - path: "../sm-chatbots-plugin/http_tracer_test.go"
+ provides: "/ping ephemeral tracer"
+ contains: "TestHTTPTracer"
+ key_links:
+ - from: "../sm-chatbots-plugin/api/commands.go"
+ to: "../sm-chatbots-plugin/classes/dispatcher.go"
+ via: "actor from bouncer.User, never from the body"
+ pattern: "Dispatch"
+ - from: "../sm-chatbots-plugin/boot.go"
+ to: "../sm-chatbots-plugin/classes/deny_all.go"
+ via: "Boot Warn while BotAuthResolver is DenyAll"
+ pattern: "BotAuthResolver"
+ prohibitions:
+ - statement: "Production sources never import the AI plugin module and never take invoker id from the request body."
+ status: unresolved
+ verification: grep
+ - statement: "Reserved help cannot be register()d as a host command; registerBuiltin installs it."
+ status: unresolved
+ verification: judgment
+---
+
+## Phase Goal
+
+**As a** application that mounts the satellites, **I want to** have Chat bots on SummerCMS with a generic `/_chatbots/api/v1`, **so that** a host can register slash commands and dispatch them as the JWT actor.
+
+This plan's slice (plan 3 of 6, wave 1): `golem15.chatbots` with `/ping` ephemeral dispatch.
+
+## Decisions taken in this plan (Claude's discretion)
+
+- Package `chatbots`, PluginID `golem15.chatbots`.
+- SlashCommand interface: Trigger, Description, IsAvailable, Handle. Modes ephemeral / announce.
+- Reserved trigger `help`; register() rejects reserved + duplicates; registerBuiltin skips the reserved check.
+- Unrecognized trigger returns nil (fall through).
+- APIPrefix `/_chatbots/api/v1`, APIBucket `chatbots-api` 120/min.
+- Baseline id `202610180001_create_golem15_chatbots_baseline`.
+- Display name Chat bots / Boty czatu.
+
+## Artifacts this phase produces (this plan)
+
+- `../sm-chatbots-plugin`.
+- Types: `contracts.SlashCommand`, `contracts.BotAuthResolver`, `classes.Registry`, `classes.Dispatcher`, `classes.CommandResult`, `models.BotFlag`.
+- Funcs: `Registry.Register`, `RegisterBuiltin`, `Dispatcher.Dispatch`, `Plugin.SetBotAuthResolver`.
+- Routes: `GET /_chatbots/api/v1/commands`, `POST /_chatbots/api/v1/dispatch`.
+
+
+Create sm-chatbots-plugin with bot_flags (D-02), SlashCommandRegistry/dispatcher (D-05), deny-all BotAuthResolver (D-03), and the /ping HTTP tracer. No AI-plugin import (SC-19.1-2).
+
+Purpose: hosts register commands; the invoker is always the authenticated actor.
+Output: TestHTTPTracer dispatches /ping as JWT actor and sees ephemeral with zero chat inserts.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md
+@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
+@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
+
+
+Analog:
+- ../sm-chatchannels-plugin/plugin.go (Requires chat+user)
+- ../sm-chatchannels-plugin/classes/deny_all.go
+- ../sm-chat-plugin/contracts/registry.go (duplicate rejection)
+- ../sm-chat-plugin/api/auth.go
+- ../sm-chat-plugin/http_tracer_test.go
+- ../sm-chat-plugin/go.work (chat replace)
+- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots @ 1ecf8c8
+
+
+
+
+
+
+ Task 1: End-to-end /ping — registry, dispatcher, JWT actor, ephemeral, /help
+ D-02 bot_flags baseline and D-05 /_chatbots/api/v1 become the shared contract.
+ ../sm-chat-plugin exists (Phase 19) and ../sm-chatbots-plugin does not already exist as a foreign module.
+ ../sm-chatbots-plugin/go.mod, ../sm-chatbots-plugin/go.work, ../sm-chatbots-plugin/go.sum, ../sm-chatbots-plugin/.gitignore, ../sm-chatbots-plugin/README.md, ../sm-chatbots-plugin/plugin.go, ../sm-chatbots-plugin/boot.go, ../sm-chatbots-plugin/routes.go, ../sm-chatbots-plugin/config/config.yaml, ../sm-chatbots-plugin/lang/en/lang.yaml, ../sm-chatbots-plugin/lang/pl/lang.yaml, ../sm-chatbots-plugin/contracts/slash_command.go, ../sm-chatbots-plugin/contracts/bot_auth_resolver.go, ../sm-chatbots-plugin/classes/deny_all.go, ../sm-chatbots-plugin/classes/registry.go, ../sm-chatbots-plugin/classes/dispatcher.go, ../sm-chatbots-plugin/classes/help.go, ../sm-chatbots-plugin/models/bot_flag.go, ../sm-chatbots-plugin/models/registry.go, ../sm-chatbots-plugin/api/auth.go, ../sm-chatbots-plugin/api/errors.go, ../sm-chatbots-plugin/api/json.go, ../sm-chatbots-plugin/api/commands.go, ../sm-chatbots-plugin/updates/registry.go, ../sm-chatbots-plugin/updates/202610180001_create_golem15_chatbots_baseline.go, ../sm-chatbots-plugin/updates/baseline_test.go, ../sm-chatbots-plugin/testmain_test.go, ../sm-chatbots-plugin/harness_test.go, ../sm-chatbots-plugin/fixture_plugin_test.go, ../sm-chatbots-plugin/http_tracer_test.go
+ ../sm-chatchannels-plugin/plugin.go, ../sm-chatchannels-plugin/go.work, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chat-plugin/contracts/registry.go, ../sm-chat-plugin/api/auth.go, ../sm-chat-plugin/http_tracer_test.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/SlashCommandRegistry.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/CommandDispatcher.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/CommandResult.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/classes/commands/HelpCommand.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/contracts/SlashCommand.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/contracts/BotAuthResolver.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/updates/create_bot_flags_table.php
+ Per D-01 and D-02, create ../sm-chatbots-plugin. Analog pins; replace summercms => ../summercms.go; go.work replaces sm-user-plugin and sm-chat-plugin. Require sm-chat-plugin. Do not require the AI plugin module (SC-19.1-2).
+
+plugin.go: package chatbots, PluginID golem15.chatbots, Requires golem15.chat and golem15.user. README H1 Chat bots; plugin.name Chat bots. SetBotAuthResolver analog SetAccessResolver.
+
+Baseline 202610180001_create_golem15_chatbots_baseline: golem15_chatbots_bot_flags (id, user_id unique chatbots_bot_flags_user_unique, is_bot bool default true, timestamps, FK user_id cascade). TestBaseline greps that table.
+
+Registry (PHP SlashCommandRegistry): reserved trigger help; Register rejects reserved and duplicates; RegisterBuiltin skips the reserved check and installs HelpCommand. Dispatcher.Dispatch(input, actor, ctx) parses the trigger; unrecognized returns nil; never reads an invoker id from input (D-05 CMD-06). CommandResult modes ephemeral and announce.
+
+This tracer registers a builtin-style /ping in the test host that returns ephemeral payload pong. Production Boot registers only help via RegisterBuiltin.
+
+BotAuthResolver (D-03): DenyAll Resolve returns nil, IsElevated false. New installs DenyAll; setter nil becomes DenyAll; Boot Warn while default is active.
+
+Routes: GET /_chatbots/api/v1/commands (filter IsAvailable for the JWT actor), POST /_chatbots/api/v1/dispatch body {input}. jwt.auth + throttle:chatbots-api 120/min. Actor from bouncer.User.
+
+Test host: golem15.user + golem15.chat + golem15.chatbots. TestHTTPTracer: JWT actor POST {input:"/ping"} returns ephemeral pong; chat message count for that conversation stays 0; GET /commands includes help. TestNoGolem (short) greps production .go and go.mod for the AI plugin module path. TestAccessResolverDefault analog for BotAuthResolver DenyAll.
+
+Remote: stay local unless plan 01 already chose proceed-and-push and tea is available; never force-push.
+
+ go -C ../sm-chatbots-plugin vet ./... && go -C ../sm-chatbots-plugin test -count=1 -short ./...
+ non-zero exit, or "ok" lines absent, or a package reports FAIL
+ docker info >/dev/null 2>&1 && go -C ../sm-chatbots-plugin test -count=1 -run '^TestHTTPTracer$' -v .
+ non-zero exit: no Docker daemon (record NOT RUN), or TestHTTPTracer missing/fails, or a chat message row was inserted for /ping
+
+
+ - grep -v '^#' ../sm-chatbots-plugin/plugin.go | grep -c 'golem15.chatbots' prints at least 1.
+ - grep -n 'golem15.chat' ../sm-chatbots-plugin/plugin.go prints at least one Requires line.
+ - grep -n 'sm-golem-plugin\|golem15.golem' ../sm-chatbots-plugin/go.mod ../sm-chatbots-plugin/*.go ../sm-chatbots-plugin/classes/*.go prints 0 lines.
+ - grep -n 'ReservedHelp\|"help"' ../sm-chatbots-plugin/classes/registry.go prints at least one line.
+
+ A JWT actor can dispatch /ping ephemerally and see /help in the catalog; the AI plugin is not a dependency.
+
+
+
+ Task 2: GDPR bot_flags delete and catalog/auth expansion
+ ../sm-chatbots-plugin/boot.go, ../sm-chatbots-plugin/classes/registry.go, ../sm-chatbots-plugin/api/commands.go, ../sm-chatbots-plugin/README.md
+ ../sm-chatbots-plugin/boot.go, ../sm-chat-plugin/boot.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityCommandController.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/BotRestController.php
+ Per D-03, Listen user.Deleting and delete golem15_chatbots_bot_flags for that user_id inside ev.Tx.
+
+Document BotAuthResolver: generic API stays jwt.auth for humans; host may later resolve a bot bearer through the setter (Wavepath BotRestReachTest behavior). Deny-all means no bot principal until the host installs one. Catalog continues to filter IsAvailable.
+
+README documents Register / RegisterBuiltin, reserved help, dispatch actor rule, and that the plugin requires Chat and User only.
+
+ go -C ../sm-chatbots-plugin vet ./... && go -C ../sm-chatbots-plugin test -count=1 -short ./...
+ non-zero exit or a package reports FAIL
+
+
+ - grep -n 'DeletingEvent' ../sm-chatbots-plugin/boot.go prints at least one line.
+ - grep -n 'bot_flags' ../sm-chatbots-plugin/boot.go prints at least one line.
+ - grep -n 'sm-golem-plugin\|golem15.golem' ../sm-chatbots-plugin/go.mod ../sm-chatbots-plugin/*.go prints 0 lines.
+
+ Deleting a user removes their bot_flags row; dispatch still uses the JWT actor.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| JWT actor → POST /dispatch | untrusted input string; invoker must not come from the body |
+| BotAuthResolver | host-supplied bot bearer; default deny-all |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-19.1-07 | Elevation of privilege | api/commands.go | high | mitigate | Actor from bouncer.User only (CMD-06); TestHTTPTracer uses the token user |
+| T-19.1-11 | Elevation of privilege | classes/deny_all.go | high | mitigate | BotAuthResolver DenyAll + Boot Warn + setter |
+| T-19.1-SC | Tampering | go.mod requires | high | mitigate | Analog pins plus sm-chat-plugin; no AI plugin module |
+
+
+
+
+go -C ../sm-chatbots-plugin test -short stays green. TestHTTPTracer proves /ping ephemeral and /help listed. TestNoGolem stays green.
+
+
+
+sm-chatbots-plugin exists with bot_flags, registry/dispatcher, deny-all bot auth, and a working /ping tracer. It requires Chat and User only.
+
+
+
diff --git a/.planning/phases/19.1-chat-satellites/19.1-04-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-04-PLAN.md
new file mode 100644
index 00000000..37495983
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/19.1-04-PLAN.md
@@ -0,0 +1,258 @@
+---
+phase: 19.1-chat-satellites
+plan: 04
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - .planning/notes/2026-10-18-webpush-go.md
+ - ../sm-herald-plugin/.gitignore
+ - ../sm-herald-plugin/go.mod
+ - ../sm-herald-plugin/go.sum
+ - ../sm-herald-plugin/go.work
+ - ../sm-herald-plugin/README.md
+ - ../sm-herald-plugin/plugin.go
+ - ../sm-herald-plugin/boot.go
+ - ../sm-herald-plugin/routes.go
+ - ../sm-herald-plugin/config/config.yaml
+ - ../sm-herald-plugin/lang/en/lang.yaml
+ - ../sm-herald-plugin/lang/pl/lang.yaml
+ - ../sm-herald-plugin/contracts/herald_profile.go
+ - ../sm-herald-plugin/contracts/presence_gate.go
+ - ../sm-herald-plugin/classes/deny_all.go
+ - ../sm-herald-plugin/classes/neutral_profile.go
+ - ../sm-herald-plugin/classes/subscription_service.go
+ - ../sm-herald-plugin/classes/sender.go
+ - ../sm-herald-plugin/jobs/send.go
+ - ../sm-herald-plugin/console/vapid_generate.go
+ - ../sm-herald-plugin/models/subscription.go
+ - ../sm-herald-plugin/models/registry.go
+ - ../sm-herald-plugin/api/auth.go
+ - ../sm-herald-plugin/api/errors.go
+ - ../sm-herald-plugin/api/json.go
+ - ../sm-herald-plugin/api/push.go
+ - ../sm-herald-plugin/updates/registry.go
+ - ../sm-herald-plugin/updates/202610180001_create_golem15_herald_baseline.go
+ - ../sm-herald-plugin/updates/baseline_test.go
+ - ../sm-herald-plugin/testmain_test.go
+ - ../sm-herald-plugin/harness_test.go
+ - ../sm-herald-plugin/fixture_plugin_test.go
+ - ../sm-herald-plugin/http_tracer_test.go
+autonomous: true
+requirements: [SC-19.1-1, SC-19.1-2, D-04, D-05, D-06, D-09, D-10, D-11, D-12]
+estimate:
+ tokens: 200000
+ raw_tokens: 200000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "D-09 / SC-19.1-2: .planning/notes/2026-10-18-webpush-go.md exists and names github.com/SherClockHolmes/webpush-go v1.4.0, jwt/v5 share, Options.HTTPClient = *fetchguard.Client Do, and does not name the in-tree Web Push module as the sender."
+ - "D-10: subscribe rejects non-HTTPS and non-public hosts; send uses fetchguard PublicOnlyMode; there is no EndpointGuard type."
+ - "D-11: SendEvent and SendUnconditional Enqueue Kind golem15.herald.send on the caller tx; a rolled-back write sends 0; 410/404 prune the row."
+ - "D-12: config keys golem15.herald.vapid.{subject,public_key,private_key}; summer herald:vapid-generate prints VAPID_PUBLIC_KEY= and VAPID_PRIVATE_KEY= lines; the private key is never logged."
+ - "D-06 / D-05: POST/PATCH/DELETE /_herald/api/v1/push/subscription and GET /_herald/api/v1/push/vapid-public-key exist; PHP ships-no-routes rule is reversed in the README."
+ - "D-03: PresenceGate default DenyAll with Boot Warn; HeraldProfile default is Neutral (app name only), no Warn."
+ artifacts:
+ - path: ".planning/notes/2026-10-18-webpush-go.md"
+ provides: "webpush-go decision note before import"
+ contains: "SherClockHolmes/webpush-go"
+ - path: "../sm-herald-plugin/jobs/send.go"
+ provides: "conga send job"
+ contains: "golem15.herald.send"
+ - path: "../sm-herald-plugin/console/vapid_generate.go"
+ provides: "herald:vapid-generate"
+ contains: "herald:vapid-generate"
+ - path: "../sm-herald-plugin/http_tracer_test.go"
+ provides: "subscribe + SendUnconditional + 410 prune + rollback"
+ contains: "TestSendEventRollback"
+ key_links:
+ - from: "../sm-herald-plugin/classes/sender.go"
+ to: "modules/fetchguard/client.go"
+ via: "Options.HTTPClient = publicOnly *fetchguard.Client"
+ pattern: "HTTPClient"
+ - from: "../sm-herald-plugin/classes/sender.go"
+ to: "../sm-herald-plugin/jobs/send.go"
+ via: "Enqueue on caller tx"
+ pattern: "Enqueue"
+ prohibitions:
+ - statement: "herald never imports the in-tree Web Push module and never uses push.* config keys."
+ status: unresolved
+ verification: grep
+ - statement: "The private VAPID key is never written to slog, payloads, or README examples."
+ status: unresolved
+ verification: judgment
+ - statement: "Requires() is empty; herald does not import user models, only user ids."
+ status: unresolved
+ verification: grep
+---
+
+## Phase Goal
+
+**As a** application that mounts the satellites, **I want to** have Herald on SummerCMS with a generic `/_herald/api/v1` push API, **so that** a host can store subscriptions and send Web Push without PHP.
+
+This plan's slice (plan 4 of 6, wave 1): decision note first, then subscribe + SendUnconditional + 410 prune + rollback sends 0.
+
+## Decisions taken in this plan (Claude's discretion)
+
+- Package `herald`, PluginID `golem15.herald`, Requires empty.
+- Job Kind `golem15.herald.send`, queue `push`, MaxAttempts 1.
+- PresenceGate.ShouldSend(userID, surface, eventID) bool.
+- NeutralHeraldProfile: Icon, Badge, BaseUrl, NotificationFor, TagFor (app-name-only).
+- OBS-01 cooldown: process-local sync.Map 60s (PHP in-process fallback).
+- APIPrefix `/_herald/api/v1`, APIBucket `herald-api` 120/min.
+- Baseline id `202610180001_create_golem15_herald_baseline`.
+- Display name Herald / Herald.
+
+## Artifacts this phase produces (this plan)
+
+- Decision note `.planning/notes/2026-10-18-webpush-go.md`.
+- `../sm-herald-plugin`.
+- Types: `contracts.HeraldProfile`, `contracts.PresenceGate`, `classes.SubscriptionService`, `jobs.SendArgs`.
+- Funcs: `SendEvent`, `SendUnconditional`, `Subscribe`, `Patch`, `Unsubscribe`.
+- Command: `summer herald:vapid-generate`.
+- Config keys: `golem15.herald.vapid.subject`, `.public_key`, `.private_key`.
+
+
+Write the webpush-go decision note (D-09), then create sm-herald-plugin with subscriptions, PublicOnly send (D-10), a conga job enqueued in the caller tx (D-11), PresenceGate deny-all (D-03), and the generic push API (D-05, D-06).
+
+Purpose: a rolled-back write must push nothing; 410 must prune.
+Output: TestHTTPTracer POSTs an https subscription, SendUnconditional after commit hits the fake, 410 deletes the row, rollback sends 0.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md
+@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
+@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
+@.planning/phases/19.1-chat-satellites/COVERAGE.md
+@.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md
+
+
+Analog:
+- ../sm-chat-plugin/jobs/maintain_thread_summary.go
+- ../sm-chat-plugin/classes/service.go Enqueue on tx
+- ../sm-chatchannels-plugin/classes/deny_all.go
+- modules/fetchguard/client.go Do
+- modules/fetchguard/policy.go PublicOnlyMode
+- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald @ a749f83
+
+
+
+
+
+
+ Task 1: webpush-go decision note before any herald import
+ D-09 sits behind the plugin sender; swapping the library later is a local change.
+ .planning/notes/2026-10-18-webpush-go.md
+ .planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md, .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (D-09, Pitfall 7, Pitfall 8), .planning/phases/19.1-chat-satellites/COVERAGE.md, modules/fetchguard/client.go, modules/flare/commands.go
+ Per D-09 / SC-19.1-2, write .planning/notes/2026-10-18-webpush-go.md in the hashids-note shape BEFORE creating the herald repo or running go get. Confirm the pin with go list -m -versions github.com/SherClockHolmes/webpush-go (expect v1.4.0). The note must record: module path and v1.4.0; MIT; shares github.com/golang-jwt/jwt/v5 with the framework; Options.HTTPClient is the Do interface so *fetchguard.Client is passed directly (Pitfall 8 — no adapter type); TTL default 2419200 and urgency normal; prune 404/410; why the in-tree Web Push module is not used (different config keys and command name). Do not generate keys in this task.
+
+ test -f .planning/notes/2026-10-18-webpush-go.md && grep -q 'SherClockHolmes/webpush-go' .planning/notes/2026-10-18-webpush-go.md && grep -q 'v1.4.0' .planning/notes/2026-10-18-webpush-go.md && echo webpush-note-ready
+ "webpush-note-ready" absent or the note lacks the module path or v1.4.0
+
+
+ - The note names Options.HTTPClient and fetchguard.Client.
+ - The note names jwt/v5 as a shared transitive.
+ - grep -n 'flare\|websockets:generate-vapid-keys' .planning/notes/2026-10-18-webpush-go.md may mention them only as the rejected alternative.
+
+ The decision note exists and no herald go.mod yet requires webpush-go.
+
+
+
+ Task 2: End-to-end push — subscribe https, SendUnconditional after commit, 410 prune, rollback sends 0
+ D-02 herald baseline and D-05 /_herald/api/v1 become the shared contract; D-06 publishes routes PHP did not ship.
+ Task 1 printed webpush-note-ready. ../sm-herald-plugin does not already exist as a foreign module.
+ ../sm-herald-plugin/go.mod, ../sm-herald-plugin/go.work, ../sm-herald-plugin/go.sum, ../sm-herald-plugin/.gitignore, ../sm-herald-plugin/README.md, ../sm-herald-plugin/plugin.go, ../sm-herald-plugin/boot.go, ../sm-herald-plugin/routes.go, ../sm-herald-plugin/config/config.yaml, ../sm-herald-plugin/lang/en/lang.yaml, ../sm-herald-plugin/lang/pl/lang.yaml, ../sm-herald-plugin/classes/subscription_service.go, ../sm-herald-plugin/classes/sender.go, ../sm-herald-plugin/jobs/send.go, ../sm-herald-plugin/models/subscription.go, ../sm-herald-plugin/models/registry.go, ../sm-herald-plugin/api/auth.go, ../sm-herald-plugin/api/errors.go, ../sm-herald-plugin/api/json.go, ../sm-herald-plugin/api/push.go, ../sm-herald-plugin/updates/registry.go, ../sm-herald-plugin/updates/202610180001_create_golem15_herald_baseline.go, ../sm-herald-plugin/updates/baseline_test.go, ../sm-herald-plugin/testmain_test.go, ../sm-herald-plugin/harness_test.go, ../sm-herald-plugin/fixture_plugin_test.go, ../sm-herald-plugin/http_tracer_test.go
+ ../sm-chat-plugin/plugin.go, ../sm-chat-plugin/jobs/maintain_thread_summary.go, ../sm-chat-plugin/classes/service.go, ../sm-chat-plugin/http_tracer_test.go, modules/fetchguard/client.go, modules/fetchguard/policy.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/PushSender.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/services/SubscriptionService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/updates/create_push_subscriptions_table.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/config/herald.php, .planning/notes/2026-10-18-webpush-go.md
+ Only after the note exists, create ../sm-herald-plugin and go get github.com/SherClockHolmes/webpush-go@v1.4.0. Analog pins; replace summercms => ../summercms.go. Requires() empty. Do not import user models. Do not import the in-tree Web Push module.
+
+plugin.go: package herald, PluginID golem15.herald, HasJobs, HasRoutes, HasCommands (command in Task 3), BucketProvider. README H1 Herald; rewrite the PHP reuse contract around the generic API (D-06).
+
+Baseline 202610180001_create_golem15_herald_baseline: golem15_herald_push_subscriptions (endpoint varchar 500, keys text JSON p256dh/auth, unique (user_id, endpoint) herald_push_user_endpoint_unique, FK user_id cascade because the test host has users). TestBaseline greps the table.
+
+SubscriptionService.Subscribe(userID, endpoint, keys, ua): reject non-HTTPS and non-public hosts with the same PublicOnly policy (D-10, fail-closed). Unique (user_id, endpoint).
+
+Sender: fetchguard.NewClient PublicOnlyMode; webpush Options.HTTPClient = that client; TTL 2419200; Urgency normal. SendUnconditional Enqueue jobs.SendArgs on the caller tx (D-11), Kind golem15.herald.send. The worker sends to every subscription and prunes 404 and 410. Tests inject fetchguard.WithTransport — no extra Doer type.
+
+Routes (D-05): POST /_herald/api/v1/push/subscription jwt.auth + throttle:herald-api. Actor from token.
+
+Test host: golem15.user (JWT only) + golem15.herald. TestSubscribeEndpoint (short): http:// and a private-IP host are rejected. TestHTTPTracer (Docker): POST https subscription against a fake transport; after commit SendUnconditional hits the fake; a 410 response deletes the row. TestSendEventRollback (Docker): Enqueue inside a tx that rolls back; fake send count stays 0.
+
+Remote: stay local unless plan 01 chose proceed-and-push; never force-push.
+
+ go -C ../sm-herald-plugin vet ./... && go -C ../sm-herald-plugin test -count=1 -short ./...
+ non-zero exit, or "ok" lines absent, or a package reports FAIL
+ docker info >/dev/null 2>&1 && go -C ../sm-herald-plugin test -count=1 -run '^(TestHTTPTracer|TestSendEventRollback)$' -v .
+ non-zero exit: no Docker daemon (record NOT RUN), or either test is missing/fails, or rollback still invoked the fake send
+
+
+ - grep -n 'SherClockHolmes/webpush-go' ../sm-herald-plugin/go.mod prints one line.
+ - grep -n 'golem15.herald.send' ../sm-herald-plugin/jobs/send.go prints at least one line.
+ - grep -n 'PublicOnlyMode' ../sm-herald-plugin/classes/*.go prints at least one line.
+ - grep -n 'modules/flare' ../sm-herald-plugin/go.mod ../sm-herald-plugin/*.go ../sm-herald-plugin/classes/*.go ../sm-herald-plugin/jobs/*.go prints 0 lines.
+
+ An https subscription can be stored; a committed send hits the fake and 410 prunes; a rolled-back send hits nothing.
+
+
+
+ Task 3: PresenceGate, NeutralHeraldProfile, vapid-generate, remaining push routes
+ ../sm-herald-plugin/contracts/herald_profile.go, ../sm-herald-plugin/contracts/presence_gate.go, ../sm-herald-plugin/classes/deny_all.go, ../sm-herald-plugin/classes/neutral_profile.go, ../sm-herald-plugin/classes/sender.go, ../sm-herald-plugin/console/vapid_generate.go, ../sm-herald-plugin/api/push.go, ../sm-herald-plugin/routes.go, ../sm-herald-plugin/boot.go, ../sm-herald-plugin/plugin.go, ../sm-herald-plugin/config/config.yaml, ../sm-herald-plugin/README.md
+ ../sm-herald-plugin/classes/sender.go, ../sm-chatchannels-plugin/classes/deny_all.go, ../sm-chat-plugin/console/rewrite_context_types.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/DenyAllPresenceGate.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/classes/NeutralHeraldProfile.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/console/GenerateVapidKeys.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/config/herald.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/PushSubscriptionController.php
+ Per D-03, PresenceGate default DenyAll ShouldSend always false; SetPresenceGate; Boot Warn while DenyAll is active. HeraldProfile default NeutralHeraldProfile (app name only) — no Warn for the profile.
+
+SendEvent (D-11): Enqueue on caller tx; the worker calls PresenceGate.ShouldSend and skips when false; SendUnconditional does not consult the gate. PayloadBuilder uses HeraldProfile. OBS-01: process-local sync.Map 60s for other delivery failures.
+
+D-12: config keys golem15.herald.vapid.subject (default mailto:admin@example.com), public_key, private_key; map PHP env names VAPID_SUBJECT / VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY. Boot Warn when keys are missing; never auto-generate. Command herald:vapid-generate calls webpush GenerateVAPIDKeys and prints VAPID_PUBLIC_KEY= and VAPID_PRIVATE_KEY= lines. Do not slog the private key. Do not copy the in-tree generate-vapid command name.
+
+Remaining routes: PATCH and DELETE /_herald/api/v1/push/subscription, GET /_herald/api/v1/push/vapid-public-key (public key only). README records D-06: this plugin ships the generic API.
+
+ go -C ../sm-herald-plugin vet ./... && go -C ../sm-herald-plugin test -count=1 -short ./...
+ non-zero exit or a package reports FAIL
+
+
+ - grep -n 'herald:vapid-generate' ../sm-herald-plugin/console/vapid_generate.go prints one line.
+ - grep -n 'golem15.herald.vapid' ../sm-herald-plugin/config/config.yaml ../sm-herald-plugin/*.go prints at least one line.
+ - grep -n 'ShouldSend' ../sm-herald-plugin/contracts/presence_gate.go prints one line.
+ - grep -n 'modules/flare\|websockets:generate-vapid-keys' ../sm-herald-plugin/*.go ../sm-herald-plugin/console/*.go prints 0 lines.
+
+ PresenceGate is fail-closed, vapid-generate prints env lines, and PATCH/DELETE/GET vapid-public-key exist.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Client → POST subscription | user-supplied endpoint URL (SSRF) |
+| Send job → push service | outbound HTTPS via PublicOnly |
+| Operator → vapid-generate | private key on stdout only |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-19.1-02 | Elevation of privilege | classes/subscription_service.go, sender.go | high | mitigate | PublicOnly at subscribe and send; TestSubscribeEndpoint; no EndpointGuard seam |
+| T-19.1-06 | Information disclosure | console/vapid_generate.go | high | mitigate | Print env lines only; never slog the private key; GET vapid-public-key returns the public key |
+| T-19.1-SC | Tampering | go.mod webpush-go | high | mitigate | Note-first pin v1.4.0; package-legitimacy crates check does not apply |
+
+
+
+
+The note exists before go get. go -C ../sm-herald-plugin test -short stays green. Docker TestHTTPTracer and TestSendEventRollback prove send-after-commit, 410 prune, and rollback silence.
+
+
+
+Herald ships the generic push API, PublicOnly send, transactional enqueue, and vapid-generate. The in-tree Web Push module is unused.
+
+
+
diff --git a/.planning/phases/19.1-chat-satellites/19.1-05-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-05-PLAN.md
new file mode 100644
index 00000000..62a8fe97
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/19.1-05-PLAN.md
@@ -0,0 +1,284 @@
+---
+phase: 19.1-chat-satellites
+plan: 05
+type: execute
+wave: 2
+depends_on: []
+files_modified:
+ - .planning/notes/2026-10-18-livekit-server-sdk-go.md
+ - ../sm-chatvideo-plugin/.gitignore
+ - ../sm-chatvideo-plugin/go.mod
+ - ../sm-chatvideo-plugin/go.sum
+ - ../sm-chatvideo-plugin/go.work
+ - ../sm-chatvideo-plugin/README.md
+ - ../sm-chatvideo-plugin/plugin.go
+ - ../sm-chatvideo-plugin/boot.go
+ - ../sm-chatvideo-plugin/routes.go
+ - ../sm-chatvideo-plugin/schedule.go
+ - ../sm-chatvideo-plugin/config/config.yaml
+ - ../sm-chatvideo-plugin/lang/en/lang.yaml
+ - ../sm-chatvideo-plugin/lang/pl/lang.yaml
+ - ../sm-chatvideo-plugin/classes/livekit.go
+ - ../sm-chatvideo-plugin/classes/authorizer.go
+ - ../sm-chatvideo-plugin/classes/video_service.go
+ - ../sm-chatvideo-plugin/classes/notifier.go
+ - ../sm-chatvideo-plugin/jobs/reconcile.go
+ - ../sm-chatvideo-plugin/console/reconcile.go
+ - ../sm-chatvideo-plugin/models/session.go
+ - ../sm-chatvideo-plugin/models/participant.go
+ - ../sm-chatvideo-plugin/models/registry.go
+ - ../sm-chatvideo-plugin/api/auth.go
+ - ../sm-chatvideo-plugin/api/errors.go
+ - ../sm-chatvideo-plugin/api/json.go
+ - ../sm-chatvideo-plugin/api/video.go
+ - ../sm-chatvideo-plugin/api/call_links.go
+ - ../sm-chatvideo-plugin/updates/registry.go
+ - ../sm-chatvideo-plugin/updates/202610180001_create_golem15_chatvideo_baseline.go
+ - ../sm-chatvideo-plugin/updates/baseline_test.go
+ - ../sm-chatvideo-plugin/testmain_test.go
+ - ../sm-chatvideo-plugin/harness_test.go
+ - ../sm-chatvideo-plugin/fixture_plugin_test.go
+ - ../sm-chatvideo-plugin/http_tracer_test.go
+autonomous: true
+requirements: [SC-19.1-1, SC-19.1-2, D-04, D-05, D-07, D-13, D-14, D-15]
+estimate:
+ tokens: 200000
+ raw_tokens: 200000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "D-13 / SC-19.1-2: .planning/notes/2026-10-18-livekit-server-sdk-go.md names github.com/livekit/server-sdk-go/v2 v2.19.0, flags pion/media-sdk/buildkit, RoomAPI interface, TrustedMode for the operator URL, and jwt/v5 MapClaims if SDK grant JSON differs."
+ - "D-02: baseline 202610180001_create_golem15_chatvideo_baseline creates golem15_chatvideo_sessions (unique active_conversation_id, join_code, no updated_at, CREATED_AT=started_at) and participants; demo-seed v1.0.1 is a comment."
+ - "D-05 / D-15: paths /_chatvideo/api/v1/conversations/{id}/video/{start,join,leave,end,status} and /call-links/{code}; video-initiate 5/min on start only; authorizer splits segment 2 of video:session:{id} and never uses the segment-1 helper; capabilities prs."
+ - "D-14: summer chatvideo:reconcile-sessions plus conga Kind golem15.chatvideo.reconcile_sessions MaxAttempts 1 every 5 minutes; generic only (empty/gone room or max_duration); no GuestCall, call cards, or CallNotifier."
+ - "D-13 token golden: HS256, iss API key, sub user id string, name, iat/nbf/exp token_ttl 900, video grant room/roomJoin/canPublish/canSubscribe, optional metadata JSON avatar."
+ artifacts:
+ - path: ".planning/notes/2026-10-18-livekit-server-sdk-go.md"
+ provides: "LiveKit v2 decision note before import"
+ contains: "server-sdk-go/v2"
+ - path: "../sm-chatvideo-plugin/classes/livekit.go"
+ provides: "RoomAPI wrapper"
+ contains: "type RoomAPI interface"
+ - path: "../sm-chatvideo-plugin/classes/authorizer.go"
+ provides: "video namespace authorizer"
+ contains: "parseVideoSessionChannel"
+ - path: "../sm-chatvideo-plugin/jobs/reconcile.go"
+ provides: "generic reconciler job"
+ contains: "golem15.chatvideo.reconcile_sessions"
+ - path: "../sm-chatvideo-plugin/console/reconcile.go"
+ provides: "chatvideo:reconcile-sessions"
+ contains: "chatvideo:reconcile-sessions"
+ key_links:
+ - from: "../sm-chatvideo-plugin/boot.go"
+ to: "../sm-chatvideo-plugin/classes/authorizer.go"
+ via: "Registry.Register video"
+ pattern: "Register\\(\"video\""
+ - from: "../sm-chatvideo-plugin/classes/livekit.go"
+ to: "modules/fetchguard/policy.go"
+ via: "TrustedMode HTTP to livekit_admin_url"
+ pattern: "TrustedMode"
+ - from: "../sm-chatvideo-plugin/routes.go"
+ to: "../sm-chatvideo-plugin/api/video.go"
+ via: "video-initiate 5/min on start only"
+ pattern: "video-initiate"
+ prohibitions:
+ - statement: "Production and tests never construct lksdk.Room and tests never import pion."
+ status: unresolved
+ verification: grep
+ - statement: "The authorizer never calls the lighthouse segment-1 helper for video:session ids."
+ status: unresolved
+ verification: grep
+ - statement: "Reconciler does not import GuestCall, call cards, CallNotifier, or start_grace_seconds."
+ status: unresolved
+ verification: grep
+ - statement: "Legacy /api/video paths and bundled JS/CSS are not registered (D-07)."
+ status: unresolved
+ verification: judgment
+user_setup:
+ - service: livekit
+ why: "Operator URL and keys for a real SFU. Tests use a RoomAPI fake; no dashboard is required to execute."
+ env_vars:
+ - name: LIVEKIT_URL
+ source: "chatvideo config livekit_url; default ws://localhost:7880"
+ - name: LIVEKIT_API_KEY
+ source: "operator LiveKit project"
+ - name: LIVEKIT_API_SECRET
+ source: "operator LiveKit project"
+---
+
+## Phase Goal
+
+**As a** application that mounts the satellites, **I want to** have Chat video on SummerCMS with a generic `/_chatvideo/api/v1`, **so that** a host can start conversation-scoped LiveKit sessions without PHP.
+
+This plan's slice (plan 5 of 6, wave 2): LiveKit note first, then start session + token golden + authorizer + empty-room reconcile.
+
+## Decisions taken in this plan (Claude's discretion)
+
+- Package `chatvideo`, PluginID `golem15.chatvideo`, Requires `golem15.chat`.
+- RoomAPI: CreateRoom, DeleteRoom (missing-room no-op), RemoveParticipant, ParticipantCount (missing-room 0), ActiveRoomNames.
+- Conversation id is the primary address; optional context lookup through chat Registry.
+- Job Kind `golem15.chatvideo.reconcile_sessions`, MaxAttempts 1, pact.Every 5 minutes.
+- join_code alphabet `abcdefghijklmnopqrstuvwxyz0123456789` length 10.
+- Display name Chat video / Czat wideo.
+- After this plan's APIs exist, execute should run the security-review agent (CONTEXT discretion). This planner does not spawn it.
+
+## Artifacts this phase produces (this plan)
+
+- Decision note `.planning/notes/2026-10-18-livekit-server-sdk-go.md`.
+- `../sm-chatvideo-plugin`.
+- Types: `classes.RoomAPI`, `models.Session`, `models.Participant`.
+- Funcs: `VideoService.Start`, `Join`, `Leave`, `End`, `Status`, `Reconcile`, `AuthorizeVideo`.
+- Command: `summer chatvideo:reconcile-sessions`.
+- Config keys: livekit_url, livekit_api_key, livekit_api_secret, max_duration, token_ttl, livekit_admin_url, max_participants, screen_share.
+
+
+Write the LiveKit v2 decision note (D-13), then create sm-chatvideo-plugin with sessions, RoomAPI + TrustedMode HTTP, a video authorizer that splits segment 2 (D-15), the generic video API (D-05), and a generic reconciler (D-14).
+
+Purpose: one active session per conversation; strangers cannot subscribe video:session:{id}.
+Output: TestHTTPTracer starts a session, TestLiveKitTokenClaims matches PHP, TestVideoAuthorizer denies a stranger, reconcile of an empty room ends the session.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md
+@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
+@.planning/phases/19.1-chat-satellites/19.1-PATTERNS.md
+@.planning/phases/19.1-chat-satellites/COVERAGE.md
+@.planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md
+
+
+Analog:
+- ../sm-chat-plugin/classes/authorizer.go (parse three segments; do not use the segment-1 helper)
+- ../sm-chat-plugin/boot.go Registry.Register
+- ../sm-paymentgateway-plugin/schedule.go pact.Every
+- modules/lighthouse/channel.go (segment-1 helper — anti-analog)
+- modules/fetchguard/policy.go TrustedMode
+- Frozen PHP /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo @ 25135b4
+- Wavepath ReconcileVideoSessions.php generic part only
+
+
+
+
+
+
+ Task 1: LiveKit v2 decision note before any chatvideo import
+ D-13 sits behind RoomAPI; swapping the SDK later is a local wrapper change.
+ .planning/notes/2026-10-18-livekit-server-sdk-go.md
+ .planning/notes/2026-10-13-paymentgateway-hashids-go-hashids.md, .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (D-13, Pitfall 9), .planning/phases/19.1-chat-satellites/COVERAGE.md, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/LiveKitService.php
+ Per D-13 / SC-19.1-2, write .planning/notes/2026-10-18-livekit-server-sdk-go.md in the hashids-note shape BEFORE creating the chatvideo repo or running go get. Confirm the pin with go list -m -versions github.com/livekit/server-sdk-go/v2 (expect v2.19.0). The official module path is github.com/livekit/server-sdk-go/v2 — not the v1 path. Record: v2.19.0; Apache-2.0; transitive livekit/protocol, twirp, protobuf; flag pion, media-sdk, buildkit (Pitfall 9); production talks to RoomAPI only; tests ship a fake and must not construct lksdk.Room; HTTP to the operator URL uses fetchguard TrustedMode; token claims golden vs PHP LiveKitService.php 80-99; if protocol/auth ToJWT differs, mint with jwt/v5 MapClaims inside the same wrapper. Do not reopen the library choice.
+
+ test -f .planning/notes/2026-10-18-livekit-server-sdk-go.md && grep -q 'livekit/server-sdk-go/v2' .planning/notes/2026-10-18-livekit-server-sdk-go.md && grep -q 'v2.19.0' .planning/notes/2026-10-18-livekit-server-sdk-go.md && echo livekit-note-ready
+ "livekit-note-ready" absent or the note lacks /v2 or v2.19.0
+
+
+ - The note flags pion or media-sdk.
+ - The note names RoomAPI and TrustedMode.
+ - The note names jwt/v5 as the golden fallback.
+
+ The LiveKit note exists and no chatvideo go.mod yet requires the SDK.
+
+
+
+ Task 2: End-to-end video start — unique active session, token golden, authorizer stranger deny
+ D-02 chatvideo baseline and D-05 /_chatvideo/api/v1 become the shared contract.
+ Task 1 printed livekit-note-ready. ../sm-chat-plugin exists. ../sm-chatvideo-plugin does not already exist as a foreign module.
+ ../sm-chatvideo-plugin/go.mod, ../sm-chatvideo-plugin/go.work, ../sm-chatvideo-plugin/go.sum, ../sm-chatvideo-plugin/.gitignore, ../sm-chatvideo-plugin/README.md, ../sm-chatvideo-plugin/plugin.go, ../sm-chatvideo-plugin/boot.go, ../sm-chatvideo-plugin/routes.go, ../sm-chatvideo-plugin/config/config.yaml, ../sm-chatvideo-plugin/lang/en/lang.yaml, ../sm-chatvideo-plugin/lang/pl/lang.yaml, ../sm-chatvideo-plugin/classes/livekit.go, ../sm-chatvideo-plugin/classes/authorizer.go, ../sm-chatvideo-plugin/classes/video_service.go, ../sm-chatvideo-plugin/models/session.go, ../sm-chatvideo-plugin/models/participant.go, ../sm-chatvideo-plugin/models/registry.go, ../sm-chatvideo-plugin/api/auth.go, ../sm-chatvideo-plugin/api/errors.go, ../sm-chatvideo-plugin/api/json.go, ../sm-chatvideo-plugin/api/video.go, ../sm-chatvideo-plugin/updates/registry.go, ../sm-chatvideo-plugin/updates/202610180001_create_golem15_chatvideo_baseline.go, ../sm-chatvideo-plugin/updates/baseline_test.go, ../sm-chatvideo-plugin/testmain_test.go, ../sm-chatvideo-plugin/harness_test.go, ../sm-chatvideo-plugin/fixture_plugin_test.go, ../sm-chatvideo-plugin/http_tracer_test.go
+ ../sm-chat-plugin/classes/authorizer.go, ../sm-chat-plugin/boot.go, ../sm-chat-plugin/routes.go, ../sm-chat-plugin/http_tracer_test.go, modules/lighthouse/channel.go, modules/fetchguard/policy.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/Plugin.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/LiveKitService.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/VideoChannelAuthorizer.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/models/VideoSession.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/updates/, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/config/config.php, .planning/notes/2026-10-18-livekit-server-sdk-go.md
+ Only after the note exists, create ../sm-chatvideo-plugin and go get github.com/livekit/server-sdk-go/v2@v2.19.0. Analog pins; replace summercms and sm-chat-plugin and sm-user-plugin. Requires golem15.chat.
+
+plugin.go: package chatvideo, PluginID golem15.chatvideo, HasRoutes, HasJobs, HasCommands, HasSchedule (schedule in Task 3), BucketProvider. README H1 Chat video.
+
+Baseline 202610180001_create_golem15_chatvideo_baseline: sessions and participants from RESEARCH. unique active_conversation_id uniq_chatvideo_active_conversation; join_code varchar 12 unique; status pending|active|ended; no updated_at; comment demo-seed v1.0.1. Model CREATED_AT=started_at. TestBaseline greps both tables.
+
+RoomAPI interface as RESEARCH. Production client: NewRoomServiceClient with HTTP through fetchguard TrustedMode (operator URL, may be loopback). Admin URL: livekit_admin_url else rewrite wss/ws to https/http. DeleteRoom swallows missing-room (D-13). ParticipantCount missing-room returns 0. Tests: in-memory fake; do not import the WebRTC stack the note flagged; do not construct the realtime room client.
+
+Token: HS256 claims matching PHP (iss API key, sub user id string, name, iat/nbf/exp, nested video grant, optional metadata). Prefer jwt/v5 MapClaims if SDK ToJWT fails the golden. TestLiveKitTokenClaims (short).
+
+Authorizer (D-15): parseVideoSessionChannel copies parseChatConversationChannel — three segments video, session, digits. Do not call the lighthouse segment-1 helper (Pitfall 1: it would read session and yield 0). Allow only participants of the session's conversation; capabilities prs. Boot Registry.Register("video", ...). Do not re-register the user namespace. TestVideoAuthorizer (short): participant allowed, stranger denied.
+
+VideoService.Start: create LiveKit room, persist session with unique active_conversation_id, invite participants, return token. video-initiate bucket 5/min on POST .../video/start only. Paths /_chatvideo/api/v1/conversations/{id}/video/start and /join (join in this tracer enough to mint a token). jwt.auth + general chatvideo-api 120/min on the rest.
+
+TestHTTPTracer (Docker): two conversation members; start; second start on the same conversation fails the unique active index or returns the existing session; fake RoomAPI saw CreateRoom.
+
+Remote: stay local unless plan 01 chose proceed-and-push; never force-push.
+
+ go -C ../sm-chatvideo-plugin vet ./... && go -C ../sm-chatvideo-plugin test -count=1 -short ./...
+ non-zero exit, or "ok" lines absent, or a package reports FAIL
+ docker info >/dev/null 2>&1 && go -C ../sm-chatvideo-plugin test -count=1 -run '^TestHTTPTracer$' -v .
+ non-zero exit: no Docker daemon (record NOT RUN), or TestHTTPTracer missing/fails, or two actives exist for one conversation
+
+
+ - grep -n 'livekit/server-sdk-go/v2' ../sm-chatvideo-plugin/go.mod prints one line.
+ - grep -n 'parseVideoSessionChannel' ../sm-chatvideo-plugin/classes/authorizer.go prints one line.
+ - grep -n 'video-initiate' ../sm-chatvideo-plugin/routes.go prints at least one line.
+ - grep -n 'ChannelID(' ../sm-chatvideo-plugin/classes/authorizer.go prints 0 lines.
+ - grep -n 'pion' ../sm-chatvideo-plugin/*_test.go ../sm-chatvideo-plugin/classes/*_test.go prints 0 lines.
+
+ A member can start one active session; the token golden matches PHP; a stranger is denied video:session:{id}.
+
+
+
+ Task 3: leave/end/status/call-links, generic reconciler, realtime events
+ ../sm-chatvideo-plugin/classes/video_service.go, ../sm-chatvideo-plugin/classes/notifier.go, ../sm-chatvideo-plugin/jobs/reconcile.go, ../sm-chatvideo-plugin/console/reconcile.go, ../sm-chatvideo-plugin/schedule.go, ../sm-chatvideo-plugin/api/video.go, ../sm-chatvideo-plugin/api/call_links.go, ../sm-chatvideo-plugin/routes.go, ../sm-chatvideo-plugin/plugin.go, ../sm-chatvideo-plugin/boot.go, ../sm-chatvideo-plugin/README.md
+ ../sm-chatvideo-plugin/classes/video_service.go, ../sm-chat-plugin/classes/emit.go, ../sm-chat-plugin/jobs/maintain_thread_summary.go, ../sm-paymentgateway-plugin/schedule.go, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatvideo/classes/VideoCallNotifier.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/console/ReconcileVideoSessions.php, /media/nvme/dev/jz/wavepath.org/plugins/golem15/wavepath/controllers/api/CommunityVideoController.php
+ Per D-05, add leave, end, status on /_chatvideo/api/v1/conversations/{id}/video/{leave,end,status} and GET /_chatvideo/api/v1/call-links/{code} with bucket call-link 10/min. join_code alphabet abcdefghijklmnopqrstuvwxyz0123456789 length 10. Uniform 404 for unknown codes.
+
+Per D-15, notifier publishes video:call:incoming to user:{id}; accepted/declined/ended and participant joined/left to video:session:{id}. Payload keys from RESEARCH. Publish failure is logged and not raised. Do not port host close-push extras.
+
+Per D-14, Reconcile: if the LiveKit room is missing or empty, or started_at + max_duration elapsed, end the session, DeleteRoom (swallowed), emit video:call:ended. Command chatvideo:reconcile-sessions. Job Kind golem15.chatvideo.reconcile_sessions, MaxAttempts 1. Plugin.Schedule pact.Every(5 * time.Minute) on that command (5 minutes divides 24h). Keep only empty-or-gone room and max_duration. Drop host-only extras (guest rooms, chat cards, grace seconds) and the host command name.
+
+After APIs exist, note in the SUMMARY that execute should run the security-review agent (CONTEXT). Do not spawn it from this task.
+
+ go -C ../sm-chatvideo-plugin vet ./... && go -C ../sm-chatvideo-plugin test -count=1 -short ./...
+ non-zero exit or a package reports FAIL
+
+
+ - grep -n 'chatvideo:reconcile-sessions' ../sm-chatvideo-plugin/console/reconcile.go prints one line.
+ - grep -n 'golem15.chatvideo.reconcile_sessions' ../sm-chatvideo-plugin/jobs/reconcile.go prints one line.
+ - grep -n 'Every(5' ../sm-chatvideo-plugin/schedule.go prints one line.
+ - grep -n 'GuestCall\|CallNotifier\|start_grace_seconds' ../sm-chatvideo-plugin/*.go ../sm-chatvideo-plugin/classes/*.go ../sm-chatvideo-plugin/jobs/*.go prints 0 lines.
+ - grep -n 'call-links' ../sm-chatvideo-plugin/routes.go prints at least one line.
+
+ Leave/end/status and call-links work; the 5-minute reconciler ends empty or expired rooms only.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| JWT actor → /_chatvideo/api/v1 | conversation id and join_code |
+| Token mint → client | LiveKit HS256 join token is a capability grant |
+| Authorizer → subscribe | client-chosen video:session:{id} |
+| Operator URL → LiveKit | TrustedMode only (not user-supplied) |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-19.1-03 | Elevation of privilege | classes/livekit.go token mint | high | mitigate | sub = authenticated user id string; secret server-side; TestLiveKitTokenClaims |
+| T-19.1-05 | Elevation of privilege | classes/authorizer.go | high | mitigate | Split segment 2; participants only; prs; TestVideoAuthorizer |
+| T-19.1-09 | Information disclosure | api/call_links.go | medium | mitigate | call-link 10/min; uniform 404 |
+| T-19.1-SC | Tampering | go.mod livekit SDK | high | mitigate | Note-first /v2 v2.19.0; tests fake RoomAPI |
+
+
+
+
+The note exists before go get. go -C ../sm-chatvideo-plugin test -short stays green including TestLiveKitTokenClaims and TestVideoAuthorizer. TestHTTPTracer proves one active session.
+
+
+
+sm-chatvideo-plugin exists with RoomAPI, a correct video authorizer, generic start/join/leave/end/status/call-links, and a generic 5-minute reconciler. Host-only GuestCall and call cards are absent.
+
+
+
diff --git a/.planning/phases/19.1-chat-satellites/19.1-06-PLAN.md b/.planning/phases/19.1-chat-satellites/19.1-06-PLAN.md
new file mode 100644
index 00000000..70acc0cb
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/19.1-06-PLAN.md
@@ -0,0 +1,239 @@
+---
+phase: 19.1-chat-satellites
+plan: 06
+type: execute
+wave: 3
+depends_on: [19.1-01, 19.1-02, 19.1-03, 19.1-04, 19.1-05]
+files_modified:
+ - ../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md
+ - ../sm-userfriends-plugin/testdata/php_methods.txt
+ - ../sm-userfriends-plugin/php_method_map_test.go
+ - ../sm-userfriends-plugin/golden_test.go
+ - ../sm-userfriends-plugin/testdata/goldens/
+ - ../sm-forum-plugin/testdata/PHP_METHOD_MAP.md
+ - ../sm-forum-plugin/testdata/php_methods.txt
+ - ../sm-forum-plugin/php_method_map_test.go
+ - ../sm-forum-plugin/golden_test.go
+ - ../sm-forum-plugin/testdata/goldens/
+ - ../sm-chatbots-plugin/testdata/PHP_METHOD_MAP.md
+ - ../sm-chatbots-plugin/testdata/php_methods.txt
+ - ../sm-chatbots-plugin/php_method_map_test.go
+ - ../sm-chatbots-plugin/golden_test.go
+ - ../sm-chatbots-plugin/testdata/goldens/
+ - ../sm-herald-plugin/testdata/PHP_METHOD_MAP.md
+ - ../sm-herald-plugin/testdata/php_methods.txt
+ - ../sm-herald-plugin/php_method_map_test.go
+ - ../sm-herald-plugin/golden_test.go
+ - ../sm-herald-plugin/testdata/goldens/
+ - ../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md
+ - ../sm-chatvideo-plugin/testdata/php_methods.txt
+ - ../sm-chatvideo-plugin/php_method_map_test.go
+ - ../sm-chatvideo-plugin/golden_test.go
+ - ../sm-chatvideo-plugin/testdata/goldens/
+ - scripts/check-phase19.1.sh
+autonomous: false
+requirements: [SC-19.1-1, SC-19.1-4, D-04]
+estimate:
+ tokens: 180000
+ raw_tokens: 180000
+ tasks: 3
+ confidence: low
+must_haves:
+ truths:
+ - "SC-19.1-4 / D-04: each plugin has testdata/PHP_METHOD_MAP.md plus TestPHPMethodMap; counts are chatbots 22, forum 25, herald 14, userfriends 31, chatvideo 0 in-plugin (Wavepath host methods mapped to generic-API Go or explicit N/A)."
+ - "Herald EndpointGuard pin/reject rows are not-applicable (D-10) with a pointer to fetchguard tests. Components and demo seeders are not-applicable (D-07)."
+ - "Goldens cover serializers, lighthouse MemoryDriver publications, LiveKit JWT claims, and forum semantic HTML (normalized DOM, D-18)."
+ - "scripts/check-phase19.1.sh fail-closed named tests across five sibling roots plus the three decision-note paths; --self-test refuses planted skip/miss/zero output."
+ artifacts:
+ - path: "scripts/check-phase19.1.sh"
+ provides: "phase gate analog to check-phase19.sh"
+ contains: "USERFRIENDS"
+ - path: "../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md"
+ provides: "31 PHP methods mapped"
+ contains: "test_"
+ - path: "../sm-chatbots-plugin/testdata/PHP_METHOD_MAP.md"
+ provides: "22 PHP methods mapped"
+ contains: "test_"
+ - path: "../sm-forum-plugin/testdata/PHP_METHOD_MAP.md"
+ provides: "25 PHP methods mapped"
+ contains: "test_"
+ - path: "../sm-herald-plugin/testdata/PHP_METHOD_MAP.md"
+ provides: "14 PHP methods mapped"
+ contains: "test_"
+ - path: "../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md"
+ provides: "0 in-plugin plus Wavepath generic-or-N/A rows"
+ contains: "N/A"
+ key_links:
+ - from: "scripts/check-phase19.1.sh"
+ to: "../sm-userfriends-plugin/http_tracer_test.go"
+ via: "named TestHTTPTracer required in postgres stage"
+ pattern: "TestHTTPTracer"
+ - from: "../sm-herald-plugin/testdata/PHP_METHOD_MAP.md"
+ to: "modules/fetchguard"
+ via: "EndpointGuard rows not-applicable D-10"
+ pattern: "not-applicable"
+ prohibitions:
+ - statement: "This plan does not re-implement D-17, vue-community-layer, or /_wave host routes."
+ status: unresolved
+ verification: judgment
+ - statement: "Goldens never contain a VAPID private key or LiveKit API secret."
+ status: unresolved
+ verification: grep
+---
+
+## Phase Goal
+
+**As a** application that mounts the satellites, **I want to** have a fail-closed phase gate and a complete PHP-method map, **so that** every frozen test is ported or explicitly not applicable.
+
+This plan's slice (plan 6 of 6, wave 3): unit tests last — maps, goldens, `scripts/check-phase19.1.sh`.
+
+## Decisions taken in this plan (Claude's discretion)
+
+- Gate env overrides: USERFRIENDS, FORUM, CHATBOTS, HERALD, CHATVIDEO (sibling checkouts).
+- Named short tests from RESEARCH/VALIDATION: TestBaseline ×5, TestHTTPTracer ×5, TestAccessResolverDefault, TestLiveKitTokenClaims, TestVideoAuthorizer, TestSubscribeEndpoint, TestNoGolem, TestNoUserfriendsImport, TestPHPMethodMap, TestFixtureAbsentFromProduction.
+- Postgres stage: tracers + TestSendEventRollback + GDPR cascades.
+- Decision-note paths grepped for locked module path + version.
+- Execute should run the security-review agent after this plan (APIs + push + tokens + HTML + GDPR all exist). This planner does not spawn it.
+- Visible recorded choice: spec-less probe fallback was skipped in plan 01; no probe predicates here either.
+
+## Artifacts this phase produces (this plan)
+
+- Five `testdata/PHP_METHOD_MAP.md` + `php_method_map_test.go`.
+- Five `golden_test.go` + `testdata/goldens/`.
+- `scripts/check-phase19.1.sh`.
+
+
+Complete D-04 / SC-19.1-4: PHP method maps (chatbots 22, forum 25, herald 14, userfriends 31, chatvideo 0 in-plugin plus Wavepath generic-or-N/A), goldens from Wavepath, and scripts/check-phase19.1.sh.
+
+Purpose: a skipped or missing named test fails the phase gate.
+Output: bash scripts/check-phase19.1.sh --self-test and the named-test stages are green when Docker is present.
+
+
+
+@~/.claude/msd-core/workflows/execute-plan.md
+@~/.claude/msd-core/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/phases/19.1-chat-satellites/19.1-CONTEXT.md
+@.planning/phases/19.1-chat-satellites/19.1-RESEARCH.md
+@.planning/phases/19.1-chat-satellites/19.1-VALIDATION.md
+@.planning/phases/19.1-chat-satellites/COVERAGE.md
+@scripts/check-phase19.sh
+
+
+Analog:
+- ../sm-chat-plugin/php_method_map_test.go
+- ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md
+- ../sm-chat-plugin/golden_test.go
+- scripts/check-phase19.sh (stages, sibling env, fail-closed detectors)
+- Frozen PHP tests under /media/nvme/dev/jz/wavepath.org/plugins/golem15/{chatbots,forum,herald,userfriends}/tests
+- Wavepath host tests for chatvideo generic behaviors
+
+
+
+
+
+
+ Task 1: End-to-end phase gate — check-phase19.1.sh self-test plus one named map
+ Plans 01-05 have created the five sibling plugin roots with TestHTTPTracer and TestBaseline names.
+ scripts/check-phase19.1.sh, ../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md, ../sm-userfriends-plugin/testdata/php_methods.txt, ../sm-userfriends-plugin/php_method_map_test.go
+ scripts/check-phase19.sh, ../sm-chat-plugin/php_method_map_test.go, ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md, .planning/phases/19.1-chat-satellites/19.1-VALIDATION.md
+ Copy scripts/check-phase19.sh shape into scripts/check-phase19.1.sh. Stages: self-test, plugins, docs, postgres. Sibling env USERFRIENDS / FORUM / CHATBOTS / HERALD / CHATVIDEO defaulting to $ROOT/../sm-<name>-plugin. Fail-closed: a skip, miss, red, or "no tests to run" fails. --self-test plants JSON the detectors must refuse. Named short tests from VALIDATION (TestBaseline, TestHTTPTracer, TestNoGolem, TestNoUserfriendsImport, TestSubscribeEndpoint, TestLiveKitTokenClaims, TestVideoAuthorizer, TestAccessResolverDefault, TestPHPMethodMap, TestFixtureAbsentFromProduction). Postgres: the five tracers plus TestSendEventRollback and GDPR cascade names. Docs stage greps the three note paths for SherClockHolmes/webpush-go, livekit/server-sdk-go/v2, and the forum-policy bluemonday pin.
+
+Also write userfriends testdata/PHP_METHOD_MAP.md for all 31 PHP test methods (GroupPolicy, GroupVisibilityGate, GroupModels). Components and demo seeders are not-applicable (D-07). php_method_map_test.go copies the chat analog. This tracer proves the gate can require TestPHPMethodMap on one repo.
+
+ bash -n scripts/check-phase19.1.sh && bash scripts/check-phase19.1.sh --self-test
+ non-zero exit, or --self-test does not refuse planted skip/miss/zero output
+ go -C ../sm-userfriends-plugin test -count=1 -short -run '^TestPHPMethodMap$' .
+ non-zero exit, or TestPHPMethodMap missing, or "no tests to run"
+
+
+ - grep -n 'USERFRIENDS' scripts/check-phase19.1.sh prints at least one line.
+ - grep -n 'TestPHPMethodMap' scripts/check-phase19.1.sh prints at least one line.
+ - wc -l ../sm-userfriends-plugin/testdata/PHP_METHOD_MAP.md is greater than 31.
+
+ The phase gate self-test refuses planted failures, and userfriends TestPHPMethodMap is green.
+
+
+
+ Task 2: Remaining PHP method maps (chatbots 22, forum 25, herald 14, chatvideo 0+Wavepath)
+ ../sm-chatbots-plugin/testdata/PHP_METHOD_MAP.md, ../sm-chatbots-plugin/testdata/php_methods.txt, ../sm-chatbots-plugin/php_method_map_test.go, ../sm-forum-plugin/testdata/PHP_METHOD_MAP.md, ../sm-forum-plugin/testdata/php_methods.txt, ../sm-forum-plugin/php_method_map_test.go, ../sm-herald-plugin/testdata/PHP_METHOD_MAP.md, ../sm-herald-plugin/testdata/php_methods.txt, ../sm-herald-plugin/php_method_map_test.go, ../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md, ../sm-chatvideo-plugin/testdata/php_methods.txt, ../sm-chatvideo-plugin/php_method_map_test.go
+ ../sm-userfriends-plugin/php_method_map_test.go, ../sm-chat-plugin/testdata/PHP_METHOD_MAP.md, /media/nvme/dev/jz/wavepath.org/plugins/golem15/chatbots/tests, /media/nvme/dev/jz/wavepath.org/plugins/golem15/forum/tests, /media/nvme/dev/jz/wavepath.org/plugins/golem15/herald/tests, .planning/phases/19.1-chat-satellites/19.1-RESEARCH.md (Last-plan PHP-test-method mapping)
+ Per D-04, write one map per remaining plugin. Extract `function test` names from the frozen PHP trees. Status ported or not-applicable with the D-id.
+
+chatbots 22: all port (registry, dispatcher, help, deny-all, RODO, boundary, additivity).
+forum 25: all port; markdown goldens consume D-17.
+herald 14: port except EndpointGuard pin/reject → not-applicable (D-10) plus a pointer to fetchguard tests.
+chatvideo 0 in-plugin: map Wavepath CommunityChannelVideoTest, CommunityCallLinkTest, CommunityVideoSeverTest, CallNotificationLifecycleTest, GuestCallLinkTest, GuestLiveKitGatewayTest (and any other video host tests found) to generic-API Go or explicit N/A for guest rooms, call cards, and host close-push.
+
+Each repo gets php_method_map_test.go analog chat. Do not edit sm-user-plugin.
+
+ go -C ../sm-chatbots-plugin test -count=1 -short -run '^TestPHPMethodMap$' . && go -C ../sm-forum-plugin test -count=1 -short -run '^TestPHPMethodMap$' . && go -C ../sm-herald-plugin test -count=1 -short -run '^TestPHPMethodMap$' . && go -C ../sm-chatvideo-plugin test -count=1 -short -run '^TestPHPMethodMap$' .
+ non-zero exit, any TestPHPMethodMap missing, or "no tests to run"
+
+
+ - Each testdata/PHP_METHOD_MAP.md exists.
+ - grep -c 'not-applicable (D-10)' ../sm-herald-plugin/testdata/PHP_METHOD_MAP.md prints at least 1.
+ - grep -c 'N/A' ../sm-chatvideo-plugin/testdata/PHP_METHOD_MAP.md prints at least 1.
+
+ Every frozen PHP test method has a Go case or an explicit not-applicable row.
+
+
+
+ Task 3: Goldens and human capture of Wavepath payloads
+ ../sm-userfriends-plugin/golden_test.go, ../sm-userfriends-plugin/testdata/goldens/, ../sm-forum-plugin/golden_test.go, ../sm-forum-plugin/testdata/goldens/, ../sm-chatbots-plugin/golden_test.go, ../sm-chatbots-plugin/testdata/goldens/, ../sm-herald-plugin/golden_test.go, ../sm-herald-plugin/testdata/goldens/, ../sm-chatvideo-plugin/golden_test.go, ../sm-chatvideo-plugin/testdata/goldens/, scripts/check-phase19.1.sh
+ ../sm-chat-plugin/golden_test.go, ../sm-chat-plugin/testdata/goldens/, .planning/phases/19.1-chat-satellites/19.1-VALIDATION.md, .planning/phases/19.1-chat-satellites/COVERAGE.md
+ Per D-04 and D-18, add golden_test.go analog chat in each plugin. Capture serializer payloads, lighthouse MemoryDriver.Publications() where the plugin emits, LiveKit JWT claims (chatvideo), and forum semantic HTML (normalized DOM — same tags, attributes, text; not byte-identical). Forum corpus covers script, javascript: scheme, iframe, onerror, GFM tables, code blocks, links, autolinks. Adopted content_html is served stored.
+
+If a live PHP Wavepath backend is reachable, record payloads into testdata/goldens/. If not, derive fixtures from the PHP tests and mark the SUMMARY "goldens from PHP fixtures; live capture NOT RUN". Never store VAPID private material or LiveKit secrets in goldens.
+
+Add any missing named tests the gate requires. Record in the SUMMARY that execute should run the security-review agent now that the five APIs, push sender, token issuer, sanitizer boundary, and GDPR cascades exist. Do not spawn that agent here.
+
+ go -C ../sm-userfriends-plugin test -count=1 -short ./... && go -C ../sm-forum-plugin test -count=1 -short ./... && go -C ../sm-chatbots-plugin test -count=1 -short ./... && go -C ../sm-herald-plugin test -count=1 -short ./... && go -C ../sm-chatvideo-plugin test -count=1 -short ./...
+ non-zero exit or a package reports FAIL
+ bash scripts/check-phase19.1.sh plugins
+ non-zero exit, or a required named short test is skipped, missing, or red
+ Approve goldens against Wavepath if the PHP backend was available; otherwise confirm the SUMMARY records NOT RUN and PHP-fixture provenance.
+
+
+ - Each plugin has testdata/goldens/ with at least one json or html file.
+ - grep -n 'private_key\|API_SECRET' ../sm-*/testdata/goldens/* prints 0 lines (or those files do not exist).
+ - scripts/check-phase19.1.sh plugins exits 0 on a Docker-free host.
+
+ Goldens and the phase gate cover all five plugins; secrets are absent from fixtures.
+
+
+
+
+
+## Trust Boundaries
+
+| Boundary | Description |
+|----------|-------------|
+| Golden fixtures → repo | captured payloads must not include secrets |
+| Phase gate → CI | a skip must not count as green |
+
+## STRIDE Threat Register
+
+| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
+|-----------|----------|-----------|----------|-------------|-----------------|
+| T-19.1-12 | Information disclosure | testdata/goldens/ | medium | mitigate | No VAPID private key or LiveKit secret in fixtures; grep gate |
+| T-19.1-SC | Tampering | scripts/check-phase19.1.sh | high | mitigate | Fail-closed detectors; --self-test plants skip/miss/zero |
+
+After this plan, execute runs the security-review agent on the five generic APIs, the push sender, the LiveKit token issuer, the HTML sanitizer boundary, and the GDPR cascades (CONTEXT). This planner does not spawn it.
+
+
+
+
+bash scripts/check-phase19.1.sh --self-test and plugins stay green. Five TestPHPMethodMap names pass. Goldens exist without secrets.
+
+
+
+SC-19.1-4 is true: unit tests, PHP maps, goldens, and the phase gate exist. Every D-04 PHP method is ported or explicitly not applicable.
+
+
+
diff --git a/.planning/phases/19.1-chat-satellites/COVERAGE.md b/.planning/phases/19.1-chat-satellites/COVERAGE.md
new file mode 100644
index 00000000..95a9c6df
--- /dev/null
+++ b/.planning/phases/19.1-chat-satellites/COVERAGE.md
@@ -0,0 +1,72 @@
+# Phase 19.1 — Third-party API coverage
+
+Full-coverage-by-default for the two surfaces this phase integrates. Every OPT-OUT has a one-line reason. Guest call links, CallNotifier, and Wavepath `/_wave` host routes are out of scope (CONTEXT deferred; D-07 / D-14).
+
+**Libraries:** `github.com/SherClockHolmes/webpush-go` v1.4.0 (D-09); `github.com/livekit/server-sdk-go/v2` v2.19.0 RoomService (D-13).
+
+**Status:** COVER = production or test code in this phase must call or wrap it. OPT-OUT = explicitly not used.
+
+## webpush-go v1.4.0
+
+| Symbol | Status | Plan | Reason |
+|--------|--------|------|--------|
+| `HTTPClient` (`Do(*http.Request)`) | COVER | 04 | `Options.HTTPClient` is `*fetchguard.Client` (D-10); no wrapper type |
+| `Options` | COVER | 04 | TTL 2419200, urgency `normal`, VAPID keys, Subscriber, HTTPClient |
+| `Options.HTTPClient` | COVER | 04 | PublicOnly send path |
+| `Options.RecordSize` | OPT-OUT | — | PHP PushSender never set a record-size cap |
+| `Options.Subscriber` | COVER | 04 | `golem15.herald.vapid.subject` (default `mailto:admin@example.com`) |
+| `Options.Topic` | OPT-OUT | — | PHP never set Topic / collapse |
+| `Options.TTL` | COVER | 04 | PHP default 2419200, overridable per send (D-11) |
+| `Options.Urgency` | COVER | 04 | PHP default `normal`, overridable per send |
+| `Options.VAPIDPublicKey` | COVER | 04 | config `golem15.herald.vapid.public_key` |
+| `Options.VAPIDPrivateKey` | COVER | 04 | config `golem15.herald.vapid.private_key`; never logged |
+| `Options.VapidExpiration` | OPT-OUT | — | library default (now+12h) matches PHP VAPID JWT window |
+| `Keys` | COVER | 04 | stored `{p256dh, auth}` on `golem15_herald_push_subscriptions.keys` |
+| `Subscription` | COVER | 04 | endpoint + Keys from the subscription row |
+| `SendNotification` | OPT-OUT | — | unused; send always has a request context |
+| `SendNotificationWithContext` | COVER | 04 | the only send entry; prune on 404/410 |
+| `GenerateVAPIDKeys` | COVER | 04 | `summer herald:vapid-generate` prints env lines |
+| Urgency constants (`very-low`/`low`/`normal`/`high`) | COVER | 04 | default `normal`; others accepted when the caller overrides |
+| `flare` / `push.*` / `websockets:generate-vapid-keys` | OPT-OUT | — | D-09 locked webpush-go; different config and command names |
+
+## LiveKit RoomService (`server-sdk-go/v2` v2.19.0 `RoomServiceClient`)
+
+Production talks to the plugin `RoomAPI` interface only. The official client implements it. Tests ship a Twirp-free fake and must not import pion or construct `lksdk.Room`.
+
+| Symbol | Status | Plan | Reason |
+|--------|--------|------|--------|
+| `NewRoomServiceClient` | COVER | 05 | production constructor; HTTP via fetchguard TrustedMode |
+| `CreateRoom` | COVER | 05 | start session; room name `chat-conversation-{id}-{16 hex}` |
+| `ListRooms` | COVER | 05 | `RoomAPI.ActiveRoomNames` for the reconciler |
+| `DeleteRoom` | COVER | 05 | end/reconcile; missing-room is a no-op (D-13) |
+| `ListParticipants` | COVER | 05 | `RoomAPI.ParticipantCount` (missing-room → 0) |
+| `GetParticipant` | OPT-OUT | — | PHP LiveKitService never fetched one participant |
+| `RemoveParticipant` | COVER | 05 | leave path; PHP LiveKitService::removeParticipant |
+| `ForwardParticipant` | OPT-OUT | — | not in PHP LiveKitService |
+| `MoveParticipant` | OPT-OUT | — | not in PHP LiveKitService |
+| `MutePublishedTrack` | OPT-OUT | — | PHP never muted tracks from the plugin |
+| `UpdateParticipant` | OPT-OUT | — | not in PHP LiveKitService |
+| `UpdateSubscriptions` | OPT-OUT | — | not in PHP LiveKitService |
+| `UpdateRoomMetadata` | OPT-OUT | — | not in PHP LiveKitService |
+| `SendData` | OPT-OUT | — | realtime data goes through lighthouse, not LiveKit data messages |
+| `CreateToken` / `auth.NewAccessToken` / `ToJWT` | COVER | 05 | join JWT; golden vs PHP claims; jwt/v5 MapClaims if SDK grant JSON differs |
+| `NewLiveKitAPI` / `LiveKitAPI.Room` | OPT-OUT | — | `NewRoomServiceClient` is enough; extra facade unused |
+| `LiveKitAPI.Egress` | OPT-OUT | — | recording/streaming is not in the PHP plugin |
+| `LiveKitAPI.Ingress` | OPT-OUT | — | not in the PHP plugin |
+| `LiveKitAPI.SIP` | OPT-OUT | — | not in the PHP plugin |
+| `LiveKitAPI.AgentDispatch` | OPT-OUT | — | not in the PHP plugin |
+| `LiveKitAPI.Connector` | OPT-OUT | — | not in the PHP plugin |
+| `ConnectToRoom` / `lksdk.Room` | OPT-OUT | — | realtime participant client; plugin is RoomService-only |
+| pion / media-sdk / buildkit imports in tests | OPT-OUT | — | flag in the decision note; tests use `RoomAPI` fake |
+| Guest call links / `GuestCall` | OPT-OUT | — | Wavepath host; D-14 generic reconciler only |
+| CallNotifier / call cards / `start_grace_seconds` | OPT-OUT | — | Wavepath host; not in chatvideo config |
+| Wavepath `/_wave` video routes | OPT-OUT | — | deferred to the Wavepath app port |
+
+## Host surfaces explicitly out of scope
+
+| Surface | Reason |
+|---------|--------|
+| Wavepath `/_wave/api/v1/community/*` and `/_wave/api/v1/push/*` | Deferred Ideas; generic `/_x/api/v1` is the published contract (D-05) |
+| Host seams (`WaveForumAccessResolver`, `WaveBotAuthResolver`, `WaveHeraldProfile`, `WavePresenceGate`, `CommunityVideoService`) | App wiring, not satellite code |
+| `vue-community-layer` | Deferred Ideas |
+| D-17 `Render` / `Purify` implementation | Separate `/msd-quick`; forum consumes, does not vendor |