constitution · epochs · watch · epoch 3

comparison

c_cf0a514b261a (tommy-mor) vs c_1c1c8e7a2de8 (tommy-mor)

download prompt · raw event · cmp_2fba3b00c72f74

council reasoning

~anthropic/claude-sonnet-latest · winner B · 8:2 · permalink

Side B adds a real end-to-end feature (CLI `room create` command, RPC wiring, docs/guide updates) plus a genuine simplification removing the unused ThreadVisibility concept and its plumbing across events, reducer, timeline, and tests. Side A is a small, correct but minor dead-code cleanup in the reducer with a test tweak, of much smaller scope and impact.

~x-ai/grok-latest · winner B · 1:8 · permalink

B adds the end-to-end `room create` CLI path and simplifies the domain (drops ThreadVisibility/RoomState, rooms become a HashSet) with matching RPC, events, docs, and tests—real lasting product and design value. A only deletes a dead zero-ratio guard already enforced at parse/handler boundaries and adjusts one test, a correct but tiny cleanup.

openai/gpt-chat-latest · winner B · 1:4 · permalink

Side B adds a substantial new user-facing capability by introducing a CLI `room create` command, wiring it through the RPC layer, updating the server implementation, simplifying room state from a `HashMap` with unused visibility to a `HashSet`, and updating tests and documentation accordingly. Side A is a small cleanup that removes a redundant zero-ratio guard in `apply_vote` and adjusts a test to reflect existing zero-weight edge handling, which is useful but much narrower in impact.

sides

A — c_cf0a514b261a (tommy-mor)

message

[047b82bd] Remove redundant zero-ratio guard from reducer.

Zeros are already rejected at the DSL parser and browser handler;
the guard in apply_vote was dead code. The negative clamping stays
since add_edge_weight already skips weight-0 edges correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

diff preview

diff --git a/server/src/reducer.rs b/server/src/reducer.rs
index 0e36979abe0f051493038ff7e652efc7f7a0ac80..efbf7f67ff24c7a2989102fe62879b2794a27c5f 100644
--- a/server/src/reducer.rs
+++ b/server/src/reducer.rs
@@ -112,11 +112,6 @@ impl GroupState {
         if vote.ratio_right < 0 {
             vote.ratio_right = 0;
         }
-        if vote.ratio_left == 0 || vote.ratio_right == 0 {
-            // Zero on either side produces no valid edge; drop before registering items or pair.
-            return;
-        }
-
         let a_idx = self.ensure_item(&vote.a);
         let b_idx = self.ensure_item(&vote.b);
 
diff --git a/server/tests/basic.rs b/server/tests/basic.rs
index 08159f4a7f0850fd165817a4a1af4f31ced2ad76..1748769ccf7196b2d81cf556d5368c7e723f6a4e 100644
--- a/server/tests/basic.rs
+++ b/server/tests/basic.rs
@@ -533,7 +533,7 @@ fn dsl_parse_rejects_zero_zero_vote_ratio() {
 #[test]
 fn reducer_negative_ratio_clamped_to_zero() {
     let _state = ReducerState::default();
-    // GroupState::apply_vote clamps negatives to 0; when either side is 0 the vote is dropped.
+    // apply_vote clamps negatives to 0; add_edge_weight skips zero-weight edges.
     let mut group = GroupState::new();
     group.apply_vote(slugsocial_server::reducer::VoteData {
         ts: 1,
@@ -546,10 +546,9 @@ fn reducer_negative_ratio_clamped_to_zero() {
         delegate: Some("00000000-0000-0000-0000-000000000000:test:local/test".to_string()),
         thread_tag: "t".to_string(),
     });
-    // Nothing registered: zero-clamped vote is dropped before ensure_item.
-    assert!(group.idx_to_item.is_empty());
+    // Items and pair are registered; edges are absent because weight 0 is skipped.
+    assert_eq!(group.idx_to_item.len(), 2);
     assert!(group.edges.is_empty());
-    assert!(group.voted_pairs.is_empty());
 }
 
 

download full diff A

B — 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 B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.