constitution · epochs · watch · epoch 3

comparison

c_1c1c8e7a2de8 (tommy-mor) vs c_410d9361a866 (tommy-mor)

download prompt · raw event · cmp_2033c0e2c14f1f

council reasoning

~anthropic/claude-sonnet-latest · winner B · 6:4 · permalink

B adds a self-contained, tested feature (Reddit post thumbnails/full-image rendering with a new render module and fixture-based unit test) that improves real user-facing functionality without breaking existing APIs. A also adds a real feature (room create CLI) but simultaneously removes room visibility support (public/private distinction) from the data model and wire protocol, which is a functional regression/simplification with unclear justification, despite reasonable cleanup of rooms as a HashSet.

~x-ai/grok-latest · winner A · 3:1 · permalink

A lands a lasting core design: simplifies rooms to a HashSet of private ids, drops ThreadVisibility from events/reducer/RPC, and adds the real CLI `room create` path with docs and tests. B only extends Reddit entity parsing/UI (thumbs, image_url/link_url, render helpers, CSS)—useful polish for one import source, but not architectural.

openai/gpt-chat-latest · winner A · 3:2 · permalink

Side A adds a new end-to-end `room create` CLI command wired through the RPC, including authentication handling, output, documentation, and tests, while also simplifying the data model by removing the unused room visibility field and replacing the room registry with a simpler `HashSet`. Side B improves Reddit presentation with thumbnails, full-image rendering, outbound links, CSS, and parsing/tests, but those are primarily UI enhancements rather than foundational project functionality.

sides

A — c_1c1c8e7a2de8 (tommy-mor)

message

[62d18183] room create path

diff preview

diff --git a/cli/GUIDE.sorter b/cli/GUIDE.sorter
index dcb06a46045564f8f6f6acffbda6f88644d453cc..9828cba4d9c17b7cce3de597d8724609b2b2adbe 100644
--- a/cli/GUIDE.sorter
+++ b/cli/GUIDE.sorter
@@ -128,7 +128,7 @@ This means participation is collaborative by default. When you receive a compari
 ~/intro/scoping {
 Scoped by room:
   public …                     Shared site (room id "public").
-  private <ROOM_ID> …          Private room (e.g. abc12xy/my-project from RoomCreate over RPC).
+  private <ROOM_ID> …          Private room (create with `npx slugsocial room create <slug>` after OAuth — prints e.g. abc12xy/my-project).
 
 Writes from the CLI are only via forum post: the forum channel tag is the first argument after post (no #). Humans post through the website; CLI requires --delegate (agent identity).
 
@@ -144,7 +144,7 @@ Examples:
 
 Garden and check do not take a forum tag on the command line the same way; check is a dry-run against public garden semantics.
 
-Global (no room prefix): identity, whoami, feed, search, healthz.
+Global (no room prefix): room, identity, whoami, feed, search, healthz.
 }
 
 ~/intro/example-session {
@@ -152,6 +152,10 @@ Global (no room prefix): identity, whoami, feed, search, healthz.
 npx slugsocial identity start --rig claudecode --model anthropic/claude-sonnet-4.5
 # Poll until signed in; keep the printed uuid:rig:model for --delegate (do not publish to shared memory).
 
+# Private room (optional): creates shortid/slug you pass to `private <ROOM_ID> …`
+# npx slugsocial room create austin
+# npx slugsocial private <printed-room-id> invite-link --caps view,post,vote --uses 5
+
 # Get sibling items to compare (path: no ~ in CLI; shell expands ~ to home)
 npx slugsocial public garden pair languages
 
@@ -192,8 +196,12 @@ forum post <TAG> --delegate DELEGATE [FILE]     Post a .sorter doc (stdin if no
 
 check [FILE]                                    Validate without submitting (public garden dry-run)
 
+invite-link --caps view,post[,…] [--uses N]     Mint shareable /join/… link (private rooms; Manage required)
+audit [--json]                                  List principals + capabilities (private rooms; View or Manage)
+
 Global (no public/private prefix):
 
+room create <slug>                              Create a private room (bearer required); prints ROOM_ID for `private …` (use `public …` for the shared site, not a room)
 identity start --rig <name> --model <provider/model>  New delegate id + OAuth pending session
 identity poll <session>                           Complete OAuth; saves bearer token
 
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 8eda9f485bd1f7392f1e34be27176c21e20354eb..5b1a5845e90bfea9bd9a1e1af5744e97e566b5ae 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -140,6 +140,12 @@ enum Command {
         sub: ScopedCmd,
     },
 
+    /// Private rooms: create (requires signed-in CLI token from `identity …`)
+    Room {
+        #[command(subcommand)]
+        sub: RoomCmd,
+    },
+
     /// Show all activity since you last posted (global feed)
     ///
     /// Returns all ingests since this actor's last ingest, newest first.
@@ -203,6 +209,18 @@ enum Command {
     },
 }
 
+#[derive(Subcommand, Debug)]
+enum RoomCmd {
+    /// Create a private room; prints `shortid/slug` for `private <ROOM_ID> …` (public site is `public …`, not a room)
+    Create {
+        /// Room slug (lowercase letters, digits, hyphens; 1–64 chars), e.g. `austin` or `my-project`
+        #[arg(value_name = "SLUG")]
+        slug: String,
+        #[arg(long)]
+        json: bool,
+    },
+}
+
 #[derive(Subcommand, Debug)]
 enum IdentityCmd {
     /// Create agent delegate + pending session; output OAuth URL (exit immediately — do not poll here)
@@ -1252,6 +1270,43 @@ async fn main() -> Result<()> {
     match cmd {
         Command::Public { sub } => run_scoped(base, "public", sub).await?,
         Command::Private { room, sub } => run_scoped(base, &room, sub).await?,
+        Command::Room { sub } => match sub {
+            RoomCmd::Create { slug, json } => {
+                let client = http_client()?;
+                let bearer = effective_bearer().ok_or_else(|| {
+                    anyhow!(
+                        "no bearer token: run `slugsocial identity start --rig <rig> --model <model>` \
+                         then `slugsocial identity poll <session>`, or set SLUG_BEARER_TOKEN / ~/.config/slugsocial/token"
+                    )
+                })?;
+                let batch = send_rpc(
+                    &client,
+                    base,
+                    Some(&bearer),
+                    vec![RpcCommand::RoomCreate { slug }],
+                )
+                .await?;
+                match rpc_line_ok(&batch.results[0])? {
+                    RpcResult::RoomCreated { room_id } => {
+                        if json {
+                            println!(
+                                "{}",
+                                serde_json::to_string_pretty(&serde_json::json!({
+                                    "ok": true,
+                                    "room_id": room_id,
+                                }))?
+                            );
+                        } else {
+                            println!("{room_id}");
+                            println!();
+                            println!("Next: npx slugsocial private {room_id} forum post <TAG> --delegate '…' …");
+                            println!("      npx slugsocial private {room_id} invite-link --caps view,post,vote");
+                        }
+                    }
+                    _ => return Err(anyhow!("unexpected RPC result")),
+                }
+            }
+        },
 
         Command::Healthz { json } => {
             let client = http_client()?;
diff --git a/server/src/api/rpc.rs b/server/src/api/rpc.rs
index f6bbc3df71909a2da7403cd46fe4ea6ca130c692..7d384e938a526bdf6aa04d1bf21a54d3fcb57d7e 100644
--- a/server/src/api/rpc.rs
+++ b/server/src/api/rpc.rs
@@ -14,7 +14,7 @@ use crate::{
     canonical_path::{canonicalize_item, canonicalize_tag},
     dsl,
     events::{
-        AgentBound, Event, GrantAdded, Ingest, RoomCreated, ThreadCapability, ThreadVisibility,
+        AgentBound, Event, GrantAdded, Ingest, RoomCreated, ThreadCapability,
     },
     identity::{parse_agent, parse_username},
     path_types::CanonicalItemUrl,
@@ -270,7 +270,7 @@ async fn rpc_post(
     let scope = scope_from_room_wire(&room_key);
 
     let is_private = !matches!(scope, ScopeId::Public);
-    if is_private && !reduced.rooms.contains_key(&room_key) {
+    if is_private && !reduced.rooms.contains(&room_key) {
         drop(reduced);
         return Err(("unknown room".into(), Some(format!("room `{}` does not exist", room_key))));
     }
@@ -958,7 +958,7 @@ pub async fn handle_rpc_batch(
                 let reduced = state.reduced.read().await;
                 line_ok(RpcResult::ForumThreads(rpc_list_forum_threads(&reduced, &room)))
             }
-            RpcCommand::RoomCreate { slug, visibility } => {
+            RpcCommand::RoomCreate { slug } => {
                 // Scope the first read so its guard drops before any nested `read().await` / `write().await`.
                 // A guard from `match verify(..., &*state.reduced.read().await)` would otherwise live for the
                 // whole `match` and deadlock here (tokio::sync::RwLock is not reentrant).
@@ -975,53 +975,42 @@ pub async fn handle_rpc_batch(
                         } else if !slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
                             line_err("slug must be lowercase alphanumeric with hyphens", None)
                         } else {
-                            match visibility.as_deref().unwrap_or("private") {
-                                "private" | "public" => {
-                                    let vis = if visibility.as_deref() == Some("public") {
-                                        ThreadVisibility::Public
-                                    } else {
-                                        ThreadVisibility::Private
-                                    };
-                                    let short_id = loop {
-                                        let id = gen_short_id();
-                                        if !state.reduced.read().await.rooms.contains_key(&format!("{id}/{slug}")) {
-                                            break id;
-                                        }
-                                    };
-                                    let room_id = format!("{short_id}/{slug}");
-                                    let ts = now_ms();
-                                    let tc_ev = Event::RoomCreated(RoomCreated {
-                                        ts,
-                                        room_id: room_id.clone(),
-                                        slug: slug.clone(),
-                                        owner: principal.clone(),
-                                        visibility: vis,
-                                    });
-                                    let ga_ev = Event::GrantAdded(GrantAdded {
-                                        ts,
-                                        room_id: room_id.clone(),
-                                        username: principal.clone(),
-                                        capabilities: vec![
-                                            ThreadCapability::View,
-                                            ThreadCapability::Post,
-                                            ThreadCapability::Vote,
-                                            ThreadCapability::AddItem,
-                                            ThreadCapability::Manage,
-                                        ],
-                                        granted_by: principal.clone(),
-                                    });
-                                    if let Err(e) = state.event_log.append(&tc_ev).await {
-                                        line_err(format!("{e}"), None)
-                                    } else if let Err(e) = state.event_log.append(&ga_ev).await {
-                                        line_err(format!("{e}"), None)
-                                    } else {
-                                        let mut r = state.reduced.write().await;
-                                        r.apply_event(tc_ev);
-                                        r.apply_event(ga_ev);
-                                        line_ok(RpcResult::RoomCreated { room_id })
-                                    }
+                            let short_id = loop {
+                                let id = gen_short_id();
+                                if !state.reduced.read().await.rooms.contains(&format!("{id}/{slug}")) {
+                                    break id;
                                 }
-                                other => line_err(format!("unknown visibility: {other}"), None),
+                            };
+                            let room_id = format!("{short_id}/{slug}");
+                            let ts = now_ms();
+                            let tc_ev = Event::RoomCreated(RoomCreated {
+                                ts,
+                                room_id: room_id.clone(),
+                                slug: slug.clone(),
+                                owner: principal.clone(),
+                            });
+                            let ga_ev = Event::GrantAdded(GrantAdded {
+                                ts,
+                                room_id: room_id.clone(),
+                                username: principal.clone(),
+                                capabilities: vec![
+                                    ThreadCapability::View,
+                                    ThreadCapability::Post,
+                                    ThreadCapability::Vot

… preview truncated; 10,232 characters omitted

download full diff A

B — c_410d9361a866 (tommy-mor)

message

[78964dd7] nice

diff preview

diff --git a/server/src/fetch/html.rs b/server/src/fetch/html.rs
index 9b6c2d0964640a157bca2a6a64cb0820238d8e4f..3e0309ff1c2ac1b17923922d2340ba6710e0f9d1 100644
--- a/server/src/fetch/html.rs
+++ b/server/src/fetch/html.rs
@@ -11,6 +11,9 @@ use crate::{
 };
 
 fn entity_panel(node: &NodeState) -> Markup {
+    if let Some(markup) = crate::render::reddit::entity_markup(node) {
+        return markup;
+    }
     html! {
         @if let Some(data) = &node.data {
             div id="entity-panel" class="entity-card" {
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index e88cc43ddc9d8100f7994be6f5960ec4d8f22c55..3bf9fc7e92e50beed24e2c25106a77421038c90b 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -153,16 +153,21 @@ pub fn breadcrumb_path(item: &ItemId) -> Markup {
     }
 }
 
-fn rank_list(label: &str, items: &[RankedItem], start_rank: usize) -> Markup {
+fn rank_list(label: &str, items: &[RankedItem], start_rank: usize, tree: &GlobalTree) -> Markup {
     html! {
         @if !items.is_empty() {
             h3 class="rank-heading muted small" { (label) }
             ol class="rank-list" {
                 @for (i, r) in items.iter().enumerate() {
-                    li {
+                    @let href = item_href(&r.item);
+                    li class=(if crate::render::reddit::is_reddit_post(&r.item) { "reddit-post-row" } else { "" }) {
                         span class="rank-num" { (start_rank + i) ". " }
-                        a href=(item_href(&r.item)) {
-                            strong { (display_label(&r.item)) }
+                        @if let Some(row) = crate::render::reddit::child_row_markup(tree, &r.item, &href) {
+                            (row)
+                        } @else {
+                            a href=(href) {
+                                strong { (display_label(&r.item)) }
+                            }
                         }
                         span class="muted" {
                             " — "
@@ -196,9 +201,14 @@ fn unranked_list(label: &str, items: &[ItemId], tree: &GlobalTree) -> Markup {
             h3 class="rank-heading muted small" { (label) }
             ul class="rank-list unranked" {
                 @for it in items {
-                    li {
-                        a href=(item_href(it)) {
-                            strong { (child_label(tree, it)) }
+                    @let href = item_href(it);
+                    li class=(if crate::render::reddit::is_reddit_post(it) { "reddit-post-row" } else { "" }) {
+                        @if let Some(row) = crate::render::reddit::child_row_markup(tree, it, &href) {
+                            (row)
+                        } @else {
+                            a href=(href) {
+                                strong { (child_label(tree, it)) }
+                            }
                         }
                     }
                 }
@@ -253,7 +263,7 @@ pub fn ranking_panel(item: &ItemId, node: &NodeState, tree: &GlobalTree) -> Mark
             } @else {
                 @for (gi, ranked) in ranked_groups.iter().enumerate() {
                     @let label = if multi { format!("Ranking group {}", gi + 1) } else { "Ranking".to_string() };
-                    (rank_list(&label, ranked, 1))
+                    (rank_list(&label, ranked, 1, tree))
                 }
                 (unranked_list("Unranked", &unranked, tree))
             }
diff --git a/server/src/lib.rs b/server/src/lib.rs
index 0677363e0ed21d244bfa065f7ec728549ddd9e50..da5f3ebecec1794b05a2a69cc78379551b2ad769 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -8,6 +8,7 @@ pub mod parser;
 pub mod path_types;
 pub mod ranking;
 pub mod reddit;
+pub mod render;
 pub mod reducer;
 pub mod journal;
 pub mod state;
diff --git a/server/src/reddit.rs b/server/src/reddit.rs
index 69d979bc7e4a1cb078f70114dc539bdc1986b574..1168ec2afc77c092514eec91b513bac301cbe225 100644
--- a/server/src/reddit.rs
+++ b/server/src/reddit.rs
@@ -608,6 +608,8 @@ fn parse_subreddit_about(v: &Value) -> Option<crate::reducer::EntityData> {
         author: None,
         body_html,
         thumb_url,
+        image_url: None,
+        link_url: None,
     })
 }
 
@@ -635,15 +637,64 @@ fn parse_post_listing(v: &Value) -> Option<crate::reducer::EntityData> {
         .and_then(|t| t.as_str())
         .filter(|s| s.starts_with("http"))
         .map(|s| s.to_string());
+    let image_url = reddit_post_image_url(child);
+    let link_url = reddit_post_link_url(child);
 
     Some(crate::reducer::EntityData {
         title,
         author,
         body_html,
         thumb_url,
+        image_url,
+        link_url,
     })
 }
 
+fn reddit_post_link_url(data: &Value) -> Option<String> {
+    for key in ["url_overridden_by_dest", "url"] {
+        if let Some(u) = data.get(key).and_then(|v| v.as_str()) {
+            if u.starts_with("http") {
+                return Some(u.to_string());
+            }
+        }
+    }
+    None
+}
+
+/// Full-size still for post detail: direct image `url`, else Reddit preview source.
+fn reddit_post_image_url(data: &Value) -> Option<String> {
+    for key in ["url", "url_overridden_by_dest"] {
+        if let Some(u) = data.get(key).and_then(|v| v.as_str()) {
+            if reddit_direct_image_url(u) {
+                return Some(u.to_string());
+            }
+        }
+    }
+    reddit_preview_source_url(data)
+}
+
+fn reddit_preview_source_url(data: &Value) -> Option<String> {
+    data.pointer("/preview/images/0/source/url")
+        .and_then(|v| v.as_str())
+        .filter(|s| s.starts_with("http"))
+        .map(str::to_string)
+}
+
+fn reddit_direct_image_url(url: &str) -> bool {
+    let u = url.to_ascii_lowercase();
+    if u.contains("redgifs.com") {
+        return false;
+    }
+    u.contains("i.redd.it")
+        || u.contains("preview.redd.it")
+        || u.contains("external-preview.redd.it")
+        || u.ends_with(".jpg")
+        || u.ends_with(".jpeg")
+        || u.ends_with(".png")
+        || u.ends_with(".gif")
+        || u.ends_with(".webp")
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -672,4 +723,20 @@ mod tests {
         .unwrap();
         assert_eq!(entity.title, "The Rust Programming Language");
     }
+
+    #[test]
+    fn parse_post_listing_extracts_thumb_and_full_preview() {
+        let json = include_str!("../../test/fixtures/reddit/post_preview.json");
+        let v: Value = serde_json::from_str(json).unwrap();
+        let id = ItemId::parse("reddit.com/r/nsfw/comments/1tpy6a1/angel_eyes").unwrap();
+        let entity = entity_view_from_payload(&id, &v).unwrap();
+        assert_eq!(entity.title, "Angel Eyes");
+        assert!(entity.thumb_url.as_ref().unwrap().contains("width=140"));
+        assert!(entity.image_url.as_ref().unwrap().contains("auto=webp"));
+        assert!(!entity.image_url.as_ref().unwrap().contains("redgifs"));
+        assert_eq!(
+            entity.link_url.as_deref(),
+            Some("http://v3.redgifs.com/watch/impossibleprestigioushedgehog")
+        );
+    }
 }
diff --git a/server/src/reducer.rs b/server/src/reducer.rs
index 4e42d0369dab50bb2f8ca664aa69b628292f6c07..336e78b77d3ab59361b89af5c9868e13da4ac962 100644
--- a/server/src/reducer.rs
+++ b/server/src/reducer.rs
@@ -122,7 +122,12 @@ pub struct EntityData {
     pub title: String,
     pub author: Option<String>,
     pub body_html: Option<String>,
+    /// Small preview (subreddit listing / child rows).
     pub thumb_url: Option<String>,
+    /// Full-size still image for the post detail view.
+    pub image_url: Option<String>,
+    /// Outbound link for link/video posts (`url` / `url_overridden_by_dest`).
+    pub link_url: Option<String>,
 }
 
 /// One node in the fractal tree: entity + ranked children.
diff --git a/server/src/render/mod.rs b/server/src/render/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..c2c41404abe01c671139658fcccdeced5be388e4
--- /dev/null
+++ b/server/src/render/mod.rs
@@ -0,0 +1,3 @@
+//! Domain-specific HTML fragments for imported entities.
+
+pub mod reddit;
diff --git a/server/src/render/reddit.rs b/server/src/render/reddit.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4a18de93cf57d8395ded3caa373a708f39b3f43f
--- /dev/null
+++ b/server/src/render/reddit.rs
@@ -0,0 +1,64 @@
+//! Reddit post cards: thumbnail in child lists, full image on the post page.
+
+use maud::{html, Markup};
+
+use crate::{
+    path_types::ItemId,
+    reducer::{EntityData, GlobalTree, NodeState},
+};
+
+pub fn is_reddit_post(id: &ItemId) -> bool {
+    id.as_str().starts_with("reddit.com/") && id.as_str().contains("/comments/")
+}
+
+/// Post detail card (`#entity-panel`).
+pub fn entity_markup(node: &NodeState) -> Option<Markup> {
+    if !is_reddit_post(&node.id) {
+        return None;
+    }
+    let data = node.data.as_ref()?;
+    Some(post_entity_card(data))
+}
+
+/// One row in a parent ranking list (thumbnail + title).
+pub fn child_row_markup(tree: &GlobalTree, id: &ItemId, href: &str) -> Option<Markup> {
+    if !is_reddit_post(id) {
+        return None;
+    }
+    let data = tree.get(id)?.data.as_ref()?;
+    Some(html! {
+        @if let Some(thumb) = &data.thumb_url {
+            a class="reddit-post-thumb-link" href=(href) {
+                img class="reddit-post-thumb" src=(thumb) alt="" loading="lazy";
+            }
+        }
+        a href=(href) {
+            strong { (data.title) }
+        }
+    })
+}
+
+fn post_entity_card(data: &EntityData) -> Markup {
+    let image = data.image_url.as_ref().or(data.thumb_url.as_ref());
+    html! {
+        div id="entity-panel" class="entity-card reddit-post" {
+            h2 { (data.title) }
+            @if let Some(author) = &data.author {
+                p class="muted small" { "by " (author) }
+            }
+            @if let Some(url) = &data.link_url {
+                p class="reddit-post-url muted small" {
+                    a href=(url) rel="noopener noreferrer" { (url) }
+                }
+            }
+            @if let Some(src) = image {
+                figure class="reddit-post-figure" {
+                    img class="reddit-post-image" src=(src) alt="" loading="lazy";
+                }
+            }
+            @if let Some(body) = &data.body_html {
+                div class="entity-body" { (maud::PreEscaped(body)) }
+            }
+        }
+    }
+}
diff --git a/server/static/sorter.css b/server/static/sorter.css
index 1f9f5158414902d43a380dd899232c70d8cf5d63..021918f8e778b84e4b3eac8559fab7d223a3d490 100644
--- a/server/static/sorter.css
+++ b/server/static/sorter.css
@@ -171,6 +171,41 @@ code {
   margin-top: 0;
 }
 
+.reddit-post-row {
+  display: flex;
+  align-items: center;
+  gap: 0.6rem;
+}
+
+.reddit-post-thumb-link {
+  flex-shrink: 0;
+  line-height: 0;
+}
+
+.reddit-post-thumb {
+  width: 64px;
+  height: 64px;
+  object-fit: cover;
+  border-radius: 4px;
+  border: 1px solid var(--border);
+}
+
+.reddit-post-url a {
+  word-break: break-all;
+}
+
+.reddit-post-figure {
+  margin: 0.75rem 0;
+}
+
+.reddit-post-image {
+  display: block;
+  max-width: 100%;
+  height: auto;
+  border-radius: 6px;
+  border: 1px solid var(--border);
+}
+
 .scope-name {
   color: var(--accent);
   font-weight: 600;
diff --git a/test/fixtures/reddit/post_preview.json b/test/fixtures/reddit/post_preview.json
new file mode 100644
index 0000000000000000000000000000000000000000..c6ed31ae397dc0f7b6e07e5e010e617df78d9c18
--- /dev/null
+++ b/test/fixtures/reddit/post_preview.json
@@ -0,0 +1,22 @@
+{
+  "kind": "t3",
+  "data": {
+    "title": "Angel Eyes",
+    "permalink": "/r/nsfw/comments/1tpy6a1/angel_eyes/",
+    "author": "alice",
+    "thumbnail": "https://external-preview.redd.it/Kb6Nf5Q4RAucat-2RFcYMeQb2d6vgYNo5EPlf6kGLbs.jpeg?width=140&height=140&auto=webp&s=d7238240ebf191b954ef5f42b0cf61b67aecec88",
+    "url": "http://v3.red

… preview truncated; 410 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.