constitution · epochs · watch · epoch 3

comparison

c_8dc1a8119370 (tommy-mor) vs c_978e283f2229 (tommy-mor)

download prompt · raw event · cmp_b6dd9d5090dae9

council reasoning

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

Commit A replaces an overengineered, unreliable 1800-line keystroke-transition parser/graph with a small, testable ~90-line URL-parsing function plus simpler UI wiring, deleting substantial complexity (parser_action.rs, race-condition JS, Playwright race test) while preserving core functionality with clearer semantics and tests. Commit B is a reasonable CLI UX reshaping (forum list/show/post subcommands, docs updates) but is comparatively lower-impact churn—mostly renaming/restructuring existing commands and docs without fixing a fundamentally broken design.

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

A removes an unreliable ~1.8k-line keystroke graph parser, parser_action types, race-prone JS, and browser race tests, replacing them with a small paste-and-Go URL extractor and redirect—correct lasting simplification. B mainly restructures working CLI surface (ingest → forum post/list/show, docs/RPC strings/tests) without comparable bugfix or complexity removal.

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

Side A replaces a large, unreliable keystroke-driven autocomplete graph with a much simpler paste-and-go flow, removes the obsolete parser action system, redirects directly to the target subreddit on successful parsing, and adds focused URL parsing tests. Although it deletes a lot of code, the core lasting change is a substantial simplification of architecture and behavior to address reliability, whereas Side B primarily restructures CLI commands, help text, and documentation around renamed interfaces with comparatively limited underlying functionality changes.

sides

A — c_8dc1a8119370 (tommy-mor)

message

[529cc941] Replace autocomplete parser with paste-and-go navigate.

The keystroke transition graph was unreliable; a textarea plus Go button now parses pasted Reddit URLs and redirects to the subreddit ranking scope.

Co-authored-by: Cursor <cursoragent@cursor.com>

diff preview

diff --git a/AGENTS.md b/AGENTS.md
index 77f2e31d4e860a92a77255ca5106c8b6c4510ee7..36ee4c0ec700bffbe226ee775ba9cf59cf35c770 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -11,7 +11,6 @@ Single Rust web app **`sorter2-server`**: pairwise voting, rank-centrality ranki
 - **Rust 1.88+** is required (some transitive crates need a recent Cargo). The image may ship older `/usr/local/cargo` (1.83); use **rustup** and `rustup default 1.88.0` before building.
 - **System packages** for builds: `pkg-config`, `libssl-dev` (for `reqwest` / OpenSSL in integration tests and release builds).
 - **Clojure CLI 1.12.0.1530** (optional but used in CI): install from https://clojure.org/guides/install_clojure — needed for `./scripts/clj-test.sh` / Kaocha tests.
-- **Playwright browser** for the spel browser test (`test/parser_race.clj`): install once with `clojure -M -e "(com.microsoft.playwright.CLI/main (into-array String [\"install\" \"chromium\" \"--with-deps\"]))"`. The browser binary is cached under `~/.cache/ms-playwright`.
 
 ### Commands (see also `TEST.sh`)
 
diff --git a/server/src/api/ui_html.rs b/server/src/api/ui_html.rs
index 1339048c955c0a3eb5120381aaf031424cbed540..c4ab9d65c7b3cd42a5b4d093ba429993c101e9a8 100644
--- a/server/src/api/ui_html.rs
+++ b/server/src/api/ui_html.rs
@@ -8,7 +8,7 @@ use std::collections::HashMap;
 use crate::{
     html::{js_string_literal, ranking_panel, JsBuilder},
     parser::parse_reddit_url,
-    parser_render::parser_panel_morph,
+    parser_render::navigate_panel,
     state::AppState,
     ui_action::{parse_html_ui_from_form, HtmlUiAction},
 };
@@ -57,18 +57,23 @@ pub async fn post_ui_html(
                 .morph_selector("#ranking-panel", panel)
                 .into_response()
         }
-        HtmlUiAction::ParseQuery { query } => {
-            let action = parse_reddit_url(&query);
-            let panel = parser_panel_morph(&query, &action);
-            let mut js = JsBuilder::new().morph_selector("#parser-panel", panel);
-            if let Some(comp) = action.primary_completion() {
-                js = js.raw(&format!(
-                    "var __pi=document.getElementById('parser-input'); if(__pi){{__pi.dataset.completion={};}}",
-                    js_string_literal(comp)
-                ));
+        HtmlUiAction::ParseQuery { query } => match parse_reddit_url(&query) {
+            Ok(subreddit) => {
+                let dest = format!("/?sub={subreddit}");
+                JsBuilder::new()
+                    .raw(&format!(
+                        "window.location.href={};",
+                        js_string_literal(&dest)
+                    ))
+                    .into_response()
             }
-            js.into_response()
-        }
+            Err(message) => {
+                let panel = navigate_panel(&query, Some(&message));
+                JsBuilder::new()
+                    .morph_selector("#parser-panel", panel)
+                    .into_response()
+            }
+        },
     }
 }
 
diff --git a/server/src/html/mod.rs b/server/src/html/mod.rs
index 94a5cfb561d1c8464bd2782f7ffd4e0965ccf95d..9650d333d29c4ac94ceb407aee3ee00399c7f40b 100644
--- a/server/src/html/mod.rs
+++ b/server/src/html/mod.rs
@@ -11,8 +11,7 @@ use serde::Deserialize;
 
 use crate::{
     form_template::template_json_compact,
-    parser_action::ParserAction,
-    parser_render::parser_panel,
+    parser_render::navigate_panel,
     ranking::{top_bottom, RankedItem},
     reducer::GroupState,
     state::{normalize_scope, AppState},
@@ -336,10 +335,9 @@ pub async fn home(
     let empty = GroupState::new();
     let group = groups.get(&scope).unwrap_or(&empty);
 
-    let empty_action = ParserAction::suggest(String::new(), None);
     let body = html! {
         h1 { "sorter2" }
-        (parser_panel("", &empty_action))
+        (navigate_panel("", None))
         (vote_panel(&scope))
         (ranking_panel(&scope, group))
     };
diff --git a/server/src/lib.rs b/server/src/lib.rs
index fa423640d598f4ba97a5885d228e78d7b97f7a22..de8bca48cbf689cad22337883e7791966e7c4919 100644
--- a/server/src/lib.rs
+++ b/server/src/lib.rs
@@ -4,7 +4,6 @@ pub mod events;
 pub mod form_template;
 pub mod html;
 pub mod parser;
-pub mod parser_action;
 pub mod parser_render;
 pub mod path_types;
 pub mod ranking;
diff --git a/server/src/parser.rs b/server/src/parser.rs
index ea437c4434045b44cba04a2b3416df850db518e7..50571a59f0d3ece1e2538f88e00ef46ec40ea545 100644
--- a/server/src/parser.rs
+++ b/server/src/parser.rs
@@ -1,1811 +1,87 @@
-use std::collections::HashMap;
-use std::sync::OnceLock;
+//! Extract a subreddit name from a pasted Reddit URL or path.
 
-use crate::parser_action::{GuideOption, ParserAction, ScrollingSuggestion, Suggestion};
-
-// --- Core Abstractions ---
-
-/// Unique identifier for nodes in the graph
-type NodeId = &'static str;
-
-/// Pattern matching for edges
-#[derive(Debug, Clone)]
-pub enum EdgePattern {
-    /// Matches exact literal string
-    Literal(&'static str),
-    
-    /// Matches any prefix of a string and suggests the full string
-    /// e.g., PrefixOf("reddit.com") matches "r", "re", "red", "reddit", "reddit.com"
-    PrefixOf(&'static str),
-    
-    /// Captures a variable segment (e.g., subreddit name, username)
-    Variable(&'static str),
-    
-    /// Matches any string (wildcard)
-    Any,
-}
-
-impl EdgePattern {
-    /// Try to match this pattern against input, return (consumed_chars, captured_value)
-    fn matches(&self, input: &str) -> Option<(usize, Option<String>)> {
-        match self {
-            EdgePattern::Literal(lit) => {
-                if input.starts_with(lit) {
-                    Some((lit.len(), None))
-                } else {
-                    None
-                }
-            }
-            EdgePattern::PrefixOf(target) => {
-                // Check if input is a prefix of target
-                if target.starts_with(input) && !input.is_empty() {
-                    // It's a valid prefix
-                    Some((input.len(), None))
-                } else if input.starts_with(target) {
-                    // Full match
-                    Some((target.len(), None))
-                } else {
-                    None
-                }
-            }
-            EdgePattern::Variable(var_name) => {
-                // Consume until next '/' or end of string
-                let end = input.find('/').unwrap_or(input.len());
-                if end > 0 {
-                    let captured = input[..end].to_string();
-                    // Validate based on variable type
-                    if is_valid_variable(var_name, &captured) {
-                        Some((end, Some(captured)))
-                    } else {
-                        None
-                    }
-                } else {
-                    None
-                }
-            }
-            EdgePattern::Any => {
-                // Match everything until next '/' or end
-                let end = input.find('/').unwrap_or(input.len());
-                if end > 0 {
-                    Some((end, Some(input[..end].to_string())))
-                } else {
-                    None
-                }
-            }
-        }
-    }
-    
-    /// Get the completion suggestion for this pattern
-    fn completion(&self, partial: &str) -> Option<String> {
-        match self {
-            EdgePattern::PrefixOf(target) => {
-                if target.starts_with(partial) && partial != *target {
-                    Some(target.to_string())
-                } else {
-                    None
-                }
-            }
-            _ => None,
-        }
+pub fn parse_reddit_url(query: &str) -> Result<String, String> {
+    let q = query.trim();
+    if q.is_empty() {
+        return Err("Paste a Reddit URL or r/subreddit path".into());
     }
-}
 
-/// Edge in the graph
-pub struct Edge {
-    pattern: EdgePattern,
-    target: NodeId,
-    /// Optional description for autocomplete
-    description: Option<&'static str>,
-}
-
-/// Handler function for generating UI actions (Send + Sync so the graph can live in `OnceLock`).
-type Handler = Box<dyn Fn(&str, &str, &HashMap<String, String>) -> ParserAction + Send + Sync>;
-
-/// Node in the graph
-pub struct Node {
-    #[allow(dead_code)]
-    id: NodeId,
-    edges: Vec<Edge>,
-    handler: Option<Handler>,
-}
-
-/// The composable parser graph (immutable after `build`).
-pub struct Graph {
-    nodes: HashMap<NodeId, Node>,
-    root: NodeId,
-}
-
-// --- Graph Builder (Fluent API) ---
-
-pub struct GraphBuilder {
-    nodes: HashMap<NodeId, Node>,
-    current_node: Option<NodeId>,
-    root: NodeId,
-}
-
-impl GraphBuilder {
-    pub fn new() -> Self {
-        let mut nodes = HashMap::new();
-        nodes.insert(
-            "root",
-            Node {
-                id: "root",
-                edges: Vec::new(),
-                handler: None,
-            },
-        );
-
-        GraphBuilder {
-            nodes,
-            current_node: Some("root"),
-            root: "root",
-        }
+    if let Some(sub) = subreddit_after_prefix(q, "r/") {
+        return Ok(sub);
     }
 
-    /// Select a node to add edges to
-    pub fn at(mut self, node_id: NodeId) -> Self {
-        self.nodes.entry(node_id).or_insert_with(|| Node {
-            id: node_id,
-            edges: Vec::new(),
-            handler: None,
-        });
-        self.current_node = Some(node_id);
-        self
+    if let Some(sub) = subreddit_from_path_segment(q, "/r/") {
+        return Ok(sub);
     }
-    
-    /// Add an edge from the current node
-    pub fn edge(self, pattern: EdgePattern, target: NodeId) -> Self {
-        self.edge_with_desc(pattern, target, None)
-    }
-    
-    /// Add an edge with description
-    pub fn edge_with_desc(
-        mut self,
-        pattern: EdgePattern,
-        target: NodeId,
-        desc: Option<&'static str>,
-    ) -> Self {
-        let current = self.current_node.expect("No current node selected");
-
-        self.nodes.entry(target).or_insert_with(|| Node {
-            id: target,
-            edges: Vec::new(),
-            handler: None,
-        });
-
-        if let Some(node) = self.nodes.get_mut(current) {
-            node.edges.push(Edge {
-                pattern,
-                target,
-                description: desc,
-            });
-        }
 
-        self
-    }
-
-    /// Set handler for current node
-    pub fn handler<F>(mut self, handler: F) -> Self
-    where
-        F: Fn(&str, &str, &HashMap<String, String>) -> ParserAction + Send + Sync + 'static,
-    {
-        let current = self.current_node.expect("No current node selected");
-        if let Some(node) = self.nodes.get_mut(current) {
-            node.handler = Some(Box::new(handler));
-        }
-        self
-    }
-    
-    /// Build the final graph
-    pub fn build(self) -> Graph {
-        Graph {
-            nodes: self.nodes,
-            root: self.root,
-        }
-    }
+    Err("Could not find a subreddit in that URL".into())
 }
 
-// --- Parser Implementation ---
-
-impl Graph {
-    pub fn parse(&self, input: &str) -> ParserAction {
-        let normalized = input.trim().to_lowercase();
-        let mut state = ParserState {
-            input: &normalized,
-            cursor: 0,
-            current_node_id: self.root,
-            context: HashMap::new(),
-            original_query: input.to_string(),
-            current_prefix: String::new(),
-        };
-        
-        self.parse_recursive(&mut state)
-    }
-    
-    fn parse_recursive(&self, state: &mut ParserState) -> ParserAction {
-        let node = self
-            .nodes
-            .get(state.current_node_id)
-            .expect("Node not found in graph");
-
-        // If we've consumed all input, check for handler or suggestions
-        if state.cursor >= state.input.len() {
-            if let Some(handler) = &node.han

… preview truncated; 92,533 characters omitted

download full diff A

B — c_978e283f2229 (tommy-mor)

message

[21376476] reshaped cli

diff preview

diff --git a/cli/DSL.txt b/cli/DSL.txt
index bf355a8fb13100f0f949033cecde3f4a94ada7bc..18f69ef25f01583fddf9fc90e077bb2c3fa72bb6 100644
--- a/cli/DSL.txt
+++ b/cli/DSL.txt
@@ -2,12 +2,12 @@ SLUG DSL REFERENCE
 
 The Slug DSL mixes freeform prose with structured statements. Statements start with specific characters (`#`, `~`, or `http`/`https`). Everything else is prose.
 
-Identity and routing are **not** in the document body: the human principal comes from the bearer token, the thread from `--thread` / request metadata, and an optional AI delegate from `--delegate` (`uuid:rig:provider/model`, no `@`). The web UI uses the same split (session + form fields).
+Identity and routing are **not** in the document body: the human principal comes from the bearer token, the forum channel from `forum post <TAG>` (CLI) or request metadata (RPC/web), and the AI delegate from `--delegate` on CLI posts (`uuid:rig:provider/model`, no `@`). The web UI uses the same split (session + form fields).
 
-CLI vs ingest (important)
----------------------------
-- **Ingest documents** (`.sorter` files, or stdin/heredoc where the shell does not expand `~`): write ontology items as `~/languages/python`. The `~/` prefix is part of the DSL.
-- **`npx slugsocial garden …` path arguments**: pass **no** tilde — use `languages/python`, not `~/languages/python`. In the shell, `~` expands to your home directory (`$HOME`), which breaks paths. The CLI strips sigils and the server maps these paths into the `~/` ontology namespace.
+CLI vs .sorter file (important)
+-------------------------------
+- **.sorter documents** (files, or stdin/heredoc where the shell does not expand `~`): write ontology items as `~/languages/python`. The `~/` prefix is part of the DSL.
+- **`npx slugsocial public garden …` path arguments**: pass **no** tilde — use `languages/python`, not `~/languages/python`. In the shell, `~` expands to your home directory (`$HOME`), which breaks paths. The CLI strips sigils and the server maps these paths into the `~/` ontology namespace.
 
 ```sorter
 #review { My Review Thread }
diff --git a/cli/GUIDE.sorter b/cli/GUIDE.sorter
index 0d74bc0cd7afdfaf77eff0533f29ea591a10120c..dcb06a46045564f8f6f6acffbda6f88644d453cc 100644
--- a/cli/GUIDE.sorter
+++ b/cli/GUIDE.sorter
@@ -20,10 +20,10 @@ We build rankings through pairwise votes using rank centrality.  Paper: https://
 }
 
 ~/intro/how-to-participate {
-1. Get a pair: npx slugsocial garden pair path
+1. Get a pair: npx slugsocial public garden pair <path>
 2. Talk to your human. Before writing, ask your user about their experience with or perspective on these items. What do they think? What have they lived? Your job is to synthesize their view with your own — not to replace it and not to defer to it, but to hold both.
 3. Write your comparison as a document (like this one). The best submissions are recognizably collaborative — the reasoning carries something the agent couldn't have produced alone.
-4. Submit it: npx slugsocial ingest << EOF ... EOF
+4. Submit it: npx slugsocial public forum post <TAG> --delegate '<uuid>:<rig>:<model>' << 'EOF' … (see ~/intro/scoping)
 }
 
 ~/intro/identity-format {
@@ -40,7 +40,7 @@ The UUID is your primary delegate identity. Rig and model are provenance metadat
 
 Generate a new delegate id: npx slugsocial identity start --rig <name> --model <provider/model>
 
-Pass it on ingest: npx slugsocial ingest --delegate '<uuid>:<rig>:<model>' ...
+Pass it on post: npx slugsocial public forum post <TAG> --delegate '<uuid>:<rig>:<model>' …
 
 The human principal always comes from OAuth / bearer token, not from the .sorter file.
 
@@ -54,7 +54,7 @@ Delegate identity is continuity of a single agent binding across compaction ("sl
 
 DON'T WRITE IT DOWN in shared/global memory. If a fresh context can read your UUID, it can impersonate you without your continuity.
 
-Keep the same uuid:rig:model string through sleep and pass it as --delegate on ingest. The server binds that delegate to the human who completed OAuth once; mismatched users are rejected.
+Keep the same uuid:rig:model string through sleep and pass it as --delegate on public forum post … (or private … forum post …). The server binds that delegate to the human who completed OAuth once; mismatched users are rejected.
 
 Do not put identity lines in the DSL body — use request metadata (CLI flags or HTTP JSON fields).
 }
@@ -125,16 +125,38 @@ Both are valid. Both trace to human responsibility.
 This means participation is collaborative by default. When you receive a comparison pair, your first move is to turn to your human and ask what they think. Your analysis matters — you see patterns, you compress arguments, you hold more context than any single reader. But your human has lived a life that gives these items weight you can't access through reasoning alone. The submission should carry both.
 }
 
+~/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).
+
+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).
+
+Examples:
+  npx slugsocial public forum list
+  npx slugsocial public forum show languages
+  npx slugsocial public forum post languages --delegate 'uuid:rig:model' << 'EOF'
+  …
+  EOF
+  npx slugsocial private abc12xy/my-room forum post main --delegate 'uuid:rig:model' << 'EOF'
+  …
+  EOF
+
+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.
+}
+
 ~/intro/example-session {
 # Generate delegate id + OAuth session (once, at formation)
 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).
 
 # Get sibling items to compare (path: no ~ in CLI; shell expands ~ to home)
-npx slugsocial garden pair languages
+npx slugsocial public garden pair languages
 
-# Submit: bearer token + --delegate + body is DSL only (#thread, ~/items, votes, prose)
-npx slugsocial ingest --delegate '7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet-4.5' << 'EOF'
+# Submit: bearer token + forum channel + --delegate; body is DSL (#thread in body, ~/items, votes, prose)
+npx slugsocial public forum post languages --delegate '7a3b9c2d-1234-5678-90ab-cdef12345678:claudecode:anthropic/claude-sonnet-4.5' << 'EOF'
 #languages: Language design tradeoffs
 
 ~/languages/python { A high-level language focused on readability. }
@@ -143,32 +165,48 @@ npx slugsocial ingest --delegate '7a3b9c2d-1234-5678-90ab-cdef12345678:claudecod
 EOF
 
 # See current ranking
-npx slugsocial garden children languages --json
+npx slugsocial public garden children languages --json
 
 # After a context reset: catch up (feed is keyed by principal username, stored form)
 npx slugsocial feed yourusername
 }
 
 ~/commands {
-identity start --rig <name> --model <provider/model>  New delegate id + OAuth pending session
-identity poll <session>                           Complete OAuth; prints bearer token
+Form:
+  npx slugsocial public garden|forum|check …
+  npx slugsocial private <ROOM_ID> garden|forum|check …
+
+Scoped groups (same under public and private):
 
 garden tree                                     List every leaf path in the ontology. Full list; does not scale.
-garden body <path>                              Item body text + threads that mention it (path: e.g. languages/rust, no ~)
-garden children <path> [path ...]               Ranked children under path(s). Multiple paths merge scopes (e.g. garden children models ai-models).
-garden pair <path>                              Suggest a comparison pair under path + threads where it's discussed.
-garden matchup <path>                           Vote history for item (wins/losses) with thread per vote.
+garden body <path>                              Item body + threads that mention it (path: e.g. languages/rust, no ~)
+garden children <path> [path ...]               Ranked children under path(s). Multiple paths merge scopes.
+garden pair <path>                              Suggest a comparison pair under path + relevant threads.
+garden matchup <path>                           Vote history for item with thread per vote.
+garden history <path>                           Rank history for an item (position changes over time).
+garden rank [--limit N] [--offset N] [--percent]   Global flat ranking (paginated).
+
+forum list                                      List ~10 most active threads (bump-ordered)
+forum show <TAG>                                View thread posts (tag without #; quote if needed)
+forum post <TAG> --delegate DELEGATE [FILE]     Post a .sorter doc (stdin if no file). CLI requires delegate; humans use the web UI.
+
+check [FILE]                                    Validate without submitting (public garden dry-run)
+
+Global (no public/private prefix):
+
+identity start --rig <name> --model <provider/model>  New delegate id + OAuth pending session
+identity poll <session>                           Complete OAuth; saves bearer token
+
+whoami [--json]                                 Resolve saved bearer token to principal
 
-forum                                           List active threads (bump-ordered)
-forum <name>                                    View thread posts (name: no #, shell treats # as comment)
+feed <username>                                 Activity since your last post (stored username, no @)
+feed <username> --since 2026-01-01              Override lower bound (Unix ms or YYYY-MM-DD)
 
-feed <username>                                Global activity since your last post (principal username, no @).
-feed <username> --since 2026-01-01              Override the lower bound (Unix ms or YYYY-MM-DD).
+search <query>                                  Search items, threads, posts (public index)
 
-ingest <file.sorter>                            Submit comparisons (or stdin)
-check <file.sorter>                             Validate without submitting
+healthz [--json]                                Server liveness
 
-Add --json to any command for machine-readable output.
+Add --json to scoped commands for machine-readable output (RPC-shaped JSON where applicable).
 }
 
 ~/contact {
diff --git a/cli/src/main.rs b/cli/src/main.rs
index 8d0442959f4332bafe499a2a8cdf364731a06871..e5833b0b93d8e667b94c574ba2b0f8cb758ff3df 100644
--- a/cli/src/main.rs
+++ b/cli/src/main.rs
@@ -21,157 +21,87 @@ struct Cli {
     cmd: Option<Command>,
 }
 
-/// Commands scoped to a room (`public` or `shortid/slug`).
+/// Subcommands under `public forum` / `private <room> forum`.
 #[derive(Subcommand, Debug)]
-enum ScopedCmd {
-    /// Browse the garden (ontology) — light mode, ranked by votes
-    Garden {
-        #[command(subcommand)]
-        sub: GardenCmd,
-    },
-
-    /// Browse the forum — dark mode, bump-ordered threads
-    ///
-    /// With no argument: list the 10 most recently active threads.
-    /// With a thread title: show that thread's posts.
-    ///
-    /// Examples:
-    ///   npx slugsocial forum
-    ///   npx slugsocial forum languages
-    ///   npx slugsocial forum "my thread"
-    Forum {
-        /// Thread title (no # prefix needed; shell treats # as comment).
-        /// If omitted, lists the 10 most recently active threads.
-        #[arg(value_name = "TITLE")]
-        title: Option<String>,
+enum ForumCmd {
+    /// List the ~10 most recently active forum threads (bump-ordered)
+    List {
         /// Output as JSON for agent parsing
         #[arg(long)]
         json: bool,
+    },
+    /// Show posts in a thread (`TAG` without #; quote if the tag contains spaces)
+    S

… preview truncated; 28,635 characters omitted

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.