Gym Twin merge requests: Talk threads scoped to git, reviewed in 3D

No new module. Talk's page key gains a change, so a thread is either Live (main, the gym Fernando sees) or one merge request's review, and page_access answers a review with its existing owner-only rule. CI builds every MR commit into a review line in gym_twin's Revisions, which the Gym Viewer and HUD open as the merge request page in 3D. gym_twin's Mirror, which already copies Live threads to Rocket.Chat, copies review threads to GitLab MR discussions, where the Agent Loop already turns a note into a commit and a reply. N in the viewer opens a HUD draft at the crosshair with a snapshot of the view. Talk stores an image once and artifacts_host serves it by hash, so the Mirror sends links and both chats render the same copy. Merging deploys main, and Revisions tells Fernando's threads what changed. Hand-published preview pages go away.

Modules

N: drafts a snapshot in writes threads by page in authorizes each page with loads lines, builds and diffs from watches, posts replies back to posts merge notices to mirrors Live threads to links images hosted on mirrors Review threads as MR discussions to hands each note to commits + replies in runs MR and main pipelines in deploys releases (main or !N) into replaced by Gym Viewer cad/blender/scene_viewer + open(page, ?change=!N) + compare(base, head) + N → compose(anchor, shot) − N → toast, then click N: the crosshair's anchor and a canvas snapshot, drafted in the HUD; ?change = the MR page: diff, commit stepper, outdated HUD apps/talk/hud ~ __hudPage {route, change} facet · openAt · ask · resolve + compose(anchor, blobs) + attach(paste | drop) a draft shows its snapshot; Send uploads, Cancel stores nothing; Review !N chip; image rendering: out of scope Talk apps/talk · deep ~ PageRef + change ~ post(text, images[]) + upload(image) → ImageRef start · ask · resolve · watch threads keyed by page + change; an image is stored once, by sha256, in /var/lib/talk/media; anchors keep extra.view page_access molab.auth authorize(who, page) + page.change → OWNER_ONLY Live: the page's share (Fernando and Austin comment) Review: the owner policy that already exists Revisions apps/gym_twin/revisions.py ~ ingest(release, page/) + line(main | !N) + diff(build, build) + notify(merged release) a review line never flips the live site; a main release of !N posts “Changed in <title>” on the threads it cites Mirror apps/gym_twin/mirror.py Talk ⇄ RC: Live threads + Talk ⇄ MR: Review threads + images out: CDN links + uploads in: into Talk once same Store, origin ids rc:/gl:; RC image_url, GitLab ![](url): both render the one CDN copy; a review thread = a discussion artifacts_host cdn.molab.life GET /talk-media/<sha>.<ext> as is: ~/served/talk-media is a symlink to /var/lib/talk/media, like the gym twin site; public by hash, no login Rocket.Chat gym-twin room one RC thread per Live thread as is; never sees a review thread CI · Scene Build scene_support + deploy_support package(spec) → page/ ~ release.json + line, base + MR job: review release main: deploy:prod as today MR: the same page target, as a review release; BuildId has no sha, so a squash keeps it Preview pages cad/blender/scene.py publish − publish --name <preview> − copied prod threads a preview is an MR now; its review line replaces the hand-published copy GitLab gitlab.dadovida.com MR · notes · pipelines merge · webhooks truth for commits and the merge Agent Loop apps/harness GitLabSource MR note → agent turn commit · reply note as is: notes already reach the MR's agent; it pushes to the branch and answers in the discussion + new ~ changed − removed as is

Seams

Flow

LIVE · FERNANDO COMMENTS REVISE · TOMAS ASKS THE AI REVIEW IN 3D · REPEAT MERGE · DEPLOY · FERNANDO SEES Fernando Tomas Gym (viewer+HUD) Talk gym_twin CI GitLab Agent Loop N on an item: draft + snapshot types · Send upload(png) → ImageRef start(page, anchor@s0, images) watch → RC thread, image_url revise t408…t432 (my notes on Fernando's batch) c1 on a branch, MR !N MR pipeline @c1 review release c1, line !N opens ?change=!N line !N · diff(s0, c1) N on an item · Ask AI start(page+!N, @c1) watch (owner-only page) MR note + 3D link + ![](cdn url) note → agent turn pushes c2 · reply note note hook reply cites c2 MR pipeline @c2 review release c2 c1 → c2, outdated pins merge !N (squash → s1) main pipeline: deploy release s1, mr !N “Changed in …” on t408… sees s0 → s1 · no review threads until happy

Types

~PageRef · the key of every thread
site, route, subpathas today
+ change: "!N" | nullnull = Live; !1510 = that MR's review
=Grant · what a caller may do on a page
view, notes, comment, owner
Livethe page's share: Fernando and Austin comment
ReviewOWNER_ONLY, the policy page_access already has
=Anchor · where a 3D thread points
target, label"scene3d", the item's name
extra.view{layout, build, sha, revision}: the commit on screen
extra.placement{id, local}: follows the item across builds
extra.point, camerafallback position and the view
~Release · what a pipeline hands Revisions
sha, short, at, title, mras deploy.sh writes today
+ line"main" | "!N"
+ basethe main commit the MR is compared against
+Line · a sequence of commits in Revisions
+ namemain | !N
+ commits: Commit[]only commits whose builds changed
+ head: Commit
=Commit · one git commit as the gym shows it
sha, shortnever shown alone
title, at, mrthe subject is what the UI shows
builds: {layout: BuildId}
=BuildId · content address of one layout's build
16 hexsha256 of scene.glb, collision.json, checks.json, canonical manifest
no sha insidethe MR head and its squash on main share one BuildId
+ChangeSet · the diff between two builds of a layout
+ base, head: BuildId
+ added: id[]highlighted
+ removed: {id, bounds}[]ghost boxes
+ moved: {id, from, to}[]with a trail
+ changed: {id, component}[]same place, new geometry
~ChangeLogEntry · one answered comment in the page's change log
id, title, response, status, layoutsas R2's change-log.json
+ threads: ThreadId[]the Live threads it answers
~MirrorLink · the mirror Store's pairing of a thread
thread → rc:<tmid>a Live thread's Rocket.Chat root
+ thread → gl:<discussion>a review thread's MR discussion
post n ⇄ message ideach post sent once, either way
+Draft · what N hands the HUD's composer
+ anchor: Anchorthe viewer's anchorer at the crosshair
+ images: Blob[]the canvas snapshot; shown, not uploaded
+ textempty; the cursor waits there
~HudApi · the window.__hud a page talks to
extend, setFacet, facet, openAtas today
+ compose(draft)opens the composer with the draft
+ImageRef · one stored image, by content
+ sha256its name and its secret
+ urlcdn.molab.life/talk-media/<sha256>.<ext>
+ type, width, heightimage/png | jpeg | webp
~Post · one message in a thread
n, author, text, atas today
+ images: ImageRef[]shown under the text; rendering is out of scope
originrc:<id> or gl:<note> when it came from a mirror
−PreviewPage · a hand-published shary-walkthrough--<name>
− publish --namereplaced by the MR's review line
− copied threadsreplaced by the review scope