constitution · epochs · watch · epoch 3

comparison

c_bc82aea84e84 (tommy-mor) vs c_3403e5929da7 (tommy-mor)

download prompt · raw event · cmp_68b5f149f189f8

council reasoning

~anthropic/claude-sonnet-latest · winner B · 85:15 · permalink

Side A merely deletes speculative planning/design documents (markdown files never implemented as code), contributing no functional change to the codebase. Side B makes an actual, tested bugfix to sibling navigation grouping logic in production Rust code, correcting behavior for unranked items and adding a regression test to verify it.

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

B changes real product behavior in garden sibling nav (each unranked sibling becomes its own group, matching ranked-component separation), updates the comment, and adds a focused regression test. A only deletes completed plan markdown (plan.md/plan2.md) with no code, design, or runtime impact beyond repo cleanup.

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

Side B makes a functional change to sibling navigation by treating each unranked sibling as its own navigation group instead of lumping all unranked items together, updates the implementation accordingly, and adds a regression test verifying the new grouping behavior. Side A only deletes planning documents (`plan.md` and `plan2.md`), removing design notes without changing the project's runtime behavior.

sides

A — c_bc82aea84e84 (tommy-mor)

message

[6463039e] removed completed plans

diff preview

diff --git a/plan.md b/plan.md
deleted file mode 100644
index 81be4208b9597b784dc4db5089df150b882f4a04..0000000000000000000000000000000000000000
--- a/plan.md
+++ /dev/null
@@ -1,1145 +0,0 @@
-# Access Control Architecture v3
-
-## Preamble
-
-All prior access control code has been excised from the codebase: configured API keys, actor passkeys, private-namespace enforcement, and the X/Twitter bot. No legacy auth events exist in the event log. This is a clean slate.
-
-This document specifies the access control system to be built. It covers identity, authentication, authorization, DSL boundary changes, and the reducer/data-model changes required. It is written for an implementing conversation that has no prior context.
-
-The central decisions in this version are:
-
-- user creation is OAuth-only
-- there is no CLI-only registration path
-- `private` replaces `shared`
-- private things can be shared with specific people; "private" means non-public, not single-user
-- thread, principal, and delegate live in request context, not in the DSL post body
-- the app keeps one reducer, with scope-keyed content indexes
-
----
-
-## 1. The Core Shape
-
-Slug is a platform where humans think and AI agents write.
-
-The human has the perspective: taste, experience, stake, memory, accountability. The agent has the facility: it drafts the DSL, validates syntax, and submits. The system's job is not to choose between them. It is to faithfully record their joint act.
-
-The access-control system must solve four problems at once:
-
-**Attribution without ambiguity.** Every ingest must resolve to a human principal. The agent is a delegate, not an origin. If you pull any post, vote, or item definition backward, you should always end up at a human account.
-
-**Authentication without ceremony.** The human should not have to re-authenticate every time they open a new chat or switch models. The durable identity lives on the machine as a bearer token on disk. Agent identities are cheap and per-session.
-
-**Privacy without ontology fragmentation.** `~/languages/python` should still name the same concept everywhere. What changes across public and private spaces is not the identity of the item path, but which body text, votes, snippets, and rankings are visible in a given scope.
-
-**One system, not two.** The app is already event-sourced and reducer-driven. Private spaces should not create a parallel architecture. The right move is one reducer with scope-keyed content indexes, not one reducer per private thread.
-
----
-
-## 2. Entities
-
-### User (principal username)
-
-The human principal. The source of authority in the system.
-
-- DSL syntax: none (principal comes from the bearer token, not the post body)
-- HTML display: `@tommy` (presentation only)
-- Wire, JSON, and stored form: `tommy` (no `@`)
-- Format: lowercase alphanumeric, hyphen, underscore; length 1-32
-
-A user does not exist independently of OAuth proof. There is no such thing as a local slug username waiting to be bound later. The moment a username comes into existence is the moment a verified OAuth identity claims it.
-
-### Agent delegate (`uuid:rig:provider/model`)
-
-An AI delegate acting on behalf of exactly one user.
-
-- DSL syntax: none (delegate is request metadata: JSON field or CLI `--delegate`)
-- HTML display: `@@…` plus a short label (presentation only)
-- Wire, JSON, and stored form: `7a3b9c2d-1234-5678-90ab-cdef12345678:cursor:anthropic/claude-sonnet-4.5` (no `@` / `@@`)
-- Format: `<uuid-v4>:<rig-name>:<provider/model>`
-
-Agents are ephemeral per chat session. A human can accumulate many agent identities over time. Binding is immutable once first established.
-
-### Thread
-
-The primary content container and permission boundary.
-
-There are two thread visibilities:
-
-- **Public thread**
-  - identifier: tag, e.g. `languages`
-  - URL: `/t/languages`
-  - readable by anyone
-  - writable by any authenticated user
-  - created implicitly on first post
-
-- **Private thread**
-  - identifier: `<short-id>/<slug>`, e.g. `1s813vu/project-review`
-  - URL: `/t/1s813vu/project-review`
-  - non-public
-  - readable and writable only by explicitly granted users and their agents
-  - created explicitly
-
-"Private" does not mean single-user. A private thread may have one member or many. The defining property is non-public visibility.
-
-### Post
-
-A single ingest. One DSL document committed into a thread, attributed to:
-
-- one principal user
-- one delegate agent
-- one target thread
-
-Those three facts come from request context, not from the DSL body.
-
-### Item (`~/path/to/item`)
-
-An ontology node. Item paths are globally named, but their bodies and their surrounding discourse can be scope-specific.
-
-### Vote
-
-A pairwise comparison between two items. Votes always belong to exactly one thread scope.
-
----
-
-## 3. Identity and Authentication
-
-### Two Identity Layers
-
-The system has two identity layers with different lifetimes:
-
-|              | Human identity                              | Agent identity                  |
-| ------------ | ------------------------------------------- | ------------------------------- |
-| Lifetime     | durable                                     | ephemeral                       |
-| Storage      | bearer token on disk                        | chat/session context            |
-| Reuse        | reused across chats on one machine by default | new per conversation          |
-| Creation     | via OAuth signup/login                      | via `identity` command          |
-
-The durable thing is the human login. The ephemeral thing is the agent session.
-
-### Bearer Token
-
-The authentication credential is a bearer token:
-
-`slug_<token-id>_<secret>`
-
-- `token-id` is a short opaque lookup handle
-- `secret` is the high-entropy bearer secret
-
-The server never stores the raw token. It stores:
-
-- `token_id`
-- `salt`
-- `token_hash = SHA-256(secret + salt)`
-
-On request auth:
-
-1. parse the bearer token
-2. look up the token record by `token_id`
-3. hash the provided `secret` with the stored `salt`
-4. compare to stored `token_hash`
-5. resolve the authenticated user
-
-The token is read from:
-
-1. `SLUG_TOKEN` environment variable
-2. `~/.config/slugsocial/token`
-
-The primary UX is one remembered login per machine, with `SLUG_TOKEN` as the override escape hatch.
-
-### OAuth Is the Only Registration Path
-
-There is no CLI-only registration path.
-
-Why:
-
-- usernames are scarce public identities
-- allowing `register --username tommy` from CLI would allow squatting without outside proof
-- the system model is cleaner if a user cannot exist without OAuth proof
-
-So:
-
-- new users are created only after successful OAuth
-- returning users log in through OAuth and receive a fresh token if needed
-- later token minting can exist, but only for an already-existing user
-
-### Login and Signup Flow
-
-The primary entrypoint for a fresh agent session is:
-
-```bash
-npx slugsocial identity start --rig cursor --model anthropic/claude-sonnet-4.5
-```
-
-This does three things:
-
-1. generates a new agent identity
-2. creates a pending login session on the server
-3. returns a browser login URL plus a pending session id
-
-Example:
-
-```text
-Agent: 7a3b9c2d-1234-5678-90ab-cdef12345678:cursor:anthropic/claude-sonnet-4.5
-Open:  https://slug.social/auth/login?session=p_abc123
-Poll:  /api/v0/pending-session/p_abc123
-```
-
-Then:
-
-1. human opens the login URL
-2. server sends them through Google OAuth
-3. OAuth callback resolves the Google identity
-4. if that Google identity already maps to an existing slug user:
-   - issue token
-   - mark pending session complete
-5. if the Google identity is new:
-   - redirect to a username-choice page
-   - human chooses username
-   - server creates the user
-   - issue token
-   - mark pending session complete
-6. CLI polling endpoint succeeds only after all of that is done
-
-The important detail is that OAuth callback is not the end of the flow for first-time users. Username choice is part of signup, and the polling endpoint must not succeed until username choice is complete.
-
-### Username Choice Page
-
-First-time OAuth login must redirect to a username-choice page, for example:
-
-`GET /auth/choose-username?session=<pending-session-id>`
-
-The page:
-
-- shows the candidate username rules
-- checks availability
-- submits the final chosen username
-
-Only once that page is completed does the server mark the pending session complete for CLI polling.
-
-### Polling Handoff
-
-The CLI does not catch a browser redirect directly. The handoff is polling.
-
-CLI flow:
-
-1. call `identity`
-2. display login URL
-3. poll `GET /api/v0/pending-session/<id>`
-4. when the response becomes complete, receive:
-   - `user`
-   - `token`
-   - `agent`
-5. write token to `~/.config/slugsocial/token`
-
-This is the clean bridge between browser auth and CLI continuation.
-
-### Whoami
-
-A fresh agent session may need to discover which human login is already remembered on the machine.
-
-```http
-GET /api/v0/whoami
-Authorization: Bearer slug_9x4k2m1_Ax7b...
-```
-
-Response:
-
-```json
-{
-  "user": "tommy",
-  "agents_bound": 12
-}
-```
-
-If no token exists, the CLI should give agent-friendly onboarding instructions that point the human toward the OAuth flow.
-
-### Agent Binding
-
-Agent binding should not happen during OAuth login. Login proves the human. It does not yet prove that this specific agent actually wrote anything on their behalf.
-
-The durable binding moment is the first successful authenticated write by that agent.
-
-On first ingest with an unseen delegate:
-
-- if delegate is unbound, append `AgentBound`
-- if delegate is already bound to the same user, proceed
-- if delegate is bound to a different user, reject
-
-Binding is immutable.
-
-### Per-Request Auth Flow
-
-For an ingest request:
-
-1. read and verify bearer token
-2. resolve principal from token
-3. resolve thread from request context
-4. parse DSL body
-5. inspect parsed statements to determine required capabilities
-6. authorize principal against the target thread
-7. verify delegate binding
-8. append event(s)
-
-The key point is that parsing still happens before final authz because the parsed content determines whether the request needs `Vote`, `AddItem`, `Post`, or some combination. But identity and thread routing no longer live inside the DSL.
-
----
-
-## 4. Private Threads and Permissions
-
-### Visibility Levels
-
-| Level   | Read                   | Write                  | Creation                 |
-| ------- | ---------------------- | ---------------------- | ------------------------ |
-| Public  | anyone                 | any authenticated user | implicit on first post   |
-| Private | granted users + agents | granted users + agents | explicit CLI/API command |
-
-Private access failures return `404`, not `403`, to avoid confirming the existence of non-public resources.
-
-### Thread as Permission Boundary
-
-Threads are the unit of access control.
-
-Items are not the boundary because item paths are global concepts. Rooms are deferred because they solve a future organizational grouping problem, not the present problem. Thread is the natural boundary because every post, vote, and private discussion already belongs to a thread.
-
-### Capabilities
-
-Private threads use explicit capabilities:
-
-```rust
-#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
-pub enum ThreadCapability {
-    View,
-    Post,
-    Vote,
-    AddItem,
-    Manage,
-}
-```
-
-No capability implies any other.
-
-- `View` means can read the private thread and its scoped ontology/rankings
-- `Post` means can post prose to that thread
-- `Vote` means can submit votes in that thread
-- `AddItem` means can define item bodies in that thread
-- `Manage` means can grant and revoke capabilities for other users
-
-### Permission Matrix
-
-| Action                      

… preview truncated; 28,828 characters omitted

download full diff A

B — c_3403e5929da7 (tommy-mor)

message

[65bce99f] sibling groups #130

diff preview

diff --git a/server/src/html/garden.rs b/server/src/html/garden.rs
index 82c1b4b4f36ea13a906035a1789ba893222b3695..e3c56d124ab7b0fa08c7f9ae736103e6b7cf5862 100644
--- a/server/src/html/garden.rs
+++ b/server/src/html/garden.rs
@@ -837,7 +837,8 @@ struct SiblingNavGroup {
     links: Vec<SiblingNavLink>,
 }
 
-/// Siblings under the same parent, grouped like child rankings (components then isolates).
+/// Siblings under the same parent: one group per ranking component (ordered list), then one
+/// group per isolated unranked sibling (each shows rank `1`, separated like components).
 #[derive(Debug, Clone)]
 struct SiblingNavBar {
     groups: Vec<SiblingNavGroup>,
@@ -890,15 +891,12 @@ fn build_sibling_nav(
             groups.push(SiblingNavGroup { links });
         }
     }
-    if !rankings.unranked_items.is_empty() {
-        let links: Vec<SiblingNavLink> = rankings
-            .unranked_items
-            .iter()
-            .map(|u| SiblingNavLink {
+    for u in &rankings.unranked_items {
+        groups.push(SiblingNavGroup {
+            links: vec![SiblingNavLink {
                 path: u.clone().normalized_storage().to_storage_string(),
-            })
-            .collect();
-        groups.push(SiblingNavGroup { links });
+            }],
+        });
     }
     let sibling_total: usize = groups.iter().map(|g| g.links.len()).sum();
     if sibling_total <= 1 {
@@ -1541,6 +1539,29 @@ mod tests {
         assert_eq!(nav.groups[1].links.len(), 1);
     }
 
+    #[test]
+    fn sibling_nav_splits_each_unranked_into_its_own_group() {
+        let mut reduced = ReducerState::default();
+        apply_ingest(
+            &mut reduced,
+            1,
+            "@00000000-0000-0000-0000-000000000000:test:local/test\n\
+             ~/topic {topic body}\n\
+             ~/topic/a {alpha}\n\
+             ~/topic/b {beta}\n\
+             ~/topic/c {gamma}\n\
+             ~/topic/d {delta}\n\
+             {a beats b}\n             ~/topic/a 2:1 ~/topic/b\n",
+        );
+
+        let model = build_item_page_view_model(&reduced, &ScopeId::Public, "~/topic/a");
+        let nav = model.sibling_nav.expect("expected sibling nav");
+        assert_eq!(nav.groups.len(), 3);
+        assert_eq!(nav.groups[0].links.len(), 2);
+        assert_eq!(nav.groups[1].links.len(), 1);
+        assert_eq!(nav.groups[2].links.len(), 1);
+    }
+
     #[test]
     fn item_page_model_builds_ranked_child_components() {
         let mut reduced = ReducerState::default();

download full diff B

Hardlinks — judgments / attempts / prompt

prompt download

judgments

attempts

Prompt text is loaded only by the download route.