mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-07 17:09:13 -10:00
refactor(protocol): remove the unused Workroom types; contracts owns the shape
The founder decided on 2026-10-11 (codewhale-ops CURRENT_DECISIONS
section 33, "Workroom placement") that the Workroom wire shape is defined
once in codewhale-contracts. The app and platform share wire shapes only
through that repo. The shape is now cwc_core::workroom and
packages/contracts/src/workroom.js (codewhale-platform 875fe8a, mirrored in
codewhale-contracts 64e19c0, cwc-core 1.7.0).
crates/protocol/src/workroom.rs had no consumers: only lib.rs declared the
module, and nothing in the workspace referenced codewhale_protocol::workroom.
It also cannot re-export the new definition, because this public crate does
not depend on the private contracts crate. So the copy is removed rather
than kept as a second definition. RFC 3209's status now names where the
shape lives. Its "the tree carries the protocol types" sentence, which this
change makes false, is corrected.
Checks on ed3fbec plus this change: cargo fmt --all -- --check clean;
cargo check --locked -p codewhale-protocol clean; cargo test --locked -p
codewhale-protocol: 75 passed, 0 failed (lib), 18 passed, 0 failed, and
doc-tests 0 run. grep for "workroom" under crates/ returns nothing. No tests
or code comments added.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JQ5oApYVLjkFD3kNAc3yRt
This commit is contained in:
3 files changed
+11
-354
No files matched your search
@@ -18,7 +18,6 @@ pub mod ids;
|
||||
pub mod journal;
|
||||
pub mod op;
|
||||
pub mod runtime;
|
||||
pub mod workroom;
|
||||
|
||||
/// Common trait for lifecycle status enums across the protocol layer.
|
||||
///
|
||||
|
||||
@@ -1,347 +0,0 @@
|
||||
//! Workroom types — durable chat-native containers for threaded agent work.
|
||||
//!
|
||||
//! A [`Workroom`] groups threads, events, and external references into a
|
||||
//! stable, addressable surface that can be accessed from the TUI, mobile page,
|
||||
//! chat bridges, and programmatic Runtime API consumers.
|
||||
//!
|
||||
//! See `docs/rfcs/3209-workrooms.md` for the full design.
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
|
||||
/// Unique identifier for a workroom.
|
||||
///
|
||||
/// Stable across restarts. Opaque to callers; generated via UUID v4 with a
|
||||
/// `wr_` prefix for link recognition.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct WorkroomId(pub String);
|
||||
|
||||
impl WorkroomId {
|
||||
/// Create a new workroom id from a UUID v4 string.
|
||||
pub fn new() -> Self {
|
||||
Self(format!("wr_{}", uuid::Uuid::new_v4().simple()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WorkroomId {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for WorkroomId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A durable container for threaded agent conversations.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Workroom {
|
||||
pub id: WorkroomId,
|
||||
pub title: String,
|
||||
pub workspace: Option<String>,
|
||||
pub repo_identity: Option<RepoRef>,
|
||||
pub owner: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub visibility: WorkroomVisibility,
|
||||
}
|
||||
|
||||
/// GitHub repository identity attached to a workroom.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RepoRef {
|
||||
pub owner: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
/// Visibility controls for a workroom.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkroomVisibility {
|
||||
/// Only the local user can access.
|
||||
Private,
|
||||
/// Accessible to callers bearing one of the listed bearer tokens.
|
||||
Shared { allowed_tokens: Vec<String> },
|
||||
}
|
||||
|
||||
/// A thread within a workroom.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkroomThread {
|
||||
pub id: String,
|
||||
pub workroom_id: WorkroomId,
|
||||
pub title: String,
|
||||
pub kind: WorkroomThreadKind,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub external_ref: Option<ExternalThreadRef>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkroomThreadKind {
|
||||
Channel,
|
||||
DirectMessage,
|
||||
AgentTask,
|
||||
ApprovalQueue,
|
||||
ReceiptLog,
|
||||
}
|
||||
|
||||
/// An external reference that can be attached to a workroom thread.
|
||||
///
|
||||
/// Stores only metadata — no API keys, tokens, or secrets.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum ExternalThreadRef {
|
||||
GitHubIssue {
|
||||
owner: String,
|
||||
repo: String,
|
||||
number: u64,
|
||||
},
|
||||
GitHubPullRequest {
|
||||
owner: String,
|
||||
repo: String,
|
||||
number: u64,
|
||||
},
|
||||
GitHubCommit {
|
||||
owner: String,
|
||||
repo: String,
|
||||
sha: String,
|
||||
},
|
||||
GitHubCheck {
|
||||
owner: String,
|
||||
repo: String,
|
||||
check_run_id: u64,
|
||||
},
|
||||
}
|
||||
|
||||
/// An event within a workroom thread, attributed to a specific agent/model.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkroomEvent {
|
||||
pub id: String,
|
||||
pub thread_id: String,
|
||||
pub workroom_id: WorkroomId,
|
||||
pub timestamp: DateTime<Utc>,
|
||||
pub kind: WorkroomEventKind,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub agent: Option<AgentAttribution>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(tag = "event", rename_all = "snake_case")]
|
||||
pub enum WorkroomEventKind {
|
||||
Message { content: String },
|
||||
Mention { mentioned_user: String },
|
||||
ToolCall { tool_name: String, summary: String },
|
||||
ToolResult { tool_name: String, success: bool },
|
||||
ApprovalRequest { tool_name: String },
|
||||
ArtifactLinked { path: String, kind: String },
|
||||
Receipt { summary: String },
|
||||
Failure { error: String },
|
||||
NeedsHuman { reason: String },
|
||||
Resumed,
|
||||
}
|
||||
|
||||
/// Attribution metadata recording which agent and model produced an event.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AgentAttribution {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub agent_id: String,
|
||||
}
|
||||
|
||||
/// A shareable link that resolves to a workroom, thread, or event.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkroomLink {
|
||||
pub workroom_id: WorkroomId,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thread_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub event_id: Option<String>,
|
||||
}
|
||||
|
||||
impl WorkroomLink {
|
||||
/// Parse a `codewhale://workroom/...` URL.
|
||||
///
|
||||
/// Accepted forms:
|
||||
/// - `codewhale://workroom/wr_<id>`
|
||||
/// - `codewhale://workroom/wr_<id>/thread/<thread_id>`
|
||||
/// - `codewhale://workroom/wr_<id>/event/<event_id>`
|
||||
pub fn parse(url: &str) -> Option<Self> {
|
||||
let rest = url.strip_prefix("codewhale://workroom/")?;
|
||||
let mut segments = rest.split('/');
|
||||
let workroom_id = parse_segment_with_prefix(segments.next()?, "wr_")?;
|
||||
let next = segments.next();
|
||||
let (thread_id, event_id) = match next {
|
||||
None => (None, None),
|
||||
Some("thread") => {
|
||||
let thread_id = non_empty_segment(segments.next()?)?;
|
||||
match segments.next() {
|
||||
None => (Some(thread_id), None),
|
||||
Some("event") => {
|
||||
let event_id = non_empty_segment(segments.next()?)?;
|
||||
if segments.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
(Some(thread_id), Some(event_id))
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
Some("event") => {
|
||||
let event_id = non_empty_segment(segments.next()?)?;
|
||||
if segments.next().is_some() {
|
||||
return None;
|
||||
}
|
||||
(None, Some(event_id))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
workroom_id: WorkroomId(workroom_id),
|
||||
thread_id,
|
||||
event_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Serialise back to the `codewhale://workroom/...` URL form.
|
||||
pub fn to_url(&self) -> String {
|
||||
let mut url = format!("codewhale://workroom/{}", self.workroom_id);
|
||||
if let Some(ref thread_id) = self.thread_id {
|
||||
write!(url, "/thread/{thread_id}").unwrap();
|
||||
if let Some(ref event_id) = self.event_id {
|
||||
write!(url, "/event/{event_id}").unwrap();
|
||||
}
|
||||
} else if let Some(ref event_id) = self.event_id {
|
||||
write!(url, "/event/{event_id}").unwrap();
|
||||
}
|
||||
url
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_segment_with_prefix(segment: &str, prefix: &str) -> Option<String> {
|
||||
let segment = non_empty_segment(segment)?;
|
||||
if segment.len() == prefix.len() || !segment.starts_with(prefix) {
|
||||
return None;
|
||||
}
|
||||
Some(segment)
|
||||
}
|
||||
|
||||
fn non_empty_segment(segment: &str) -> Option<String> {
|
||||
if segment.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(segment.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
/// Summary projection of a workroom for list/inbox views.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkroomSummary {
|
||||
pub id: WorkroomId,
|
||||
pub title: String,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub active_threads: usize,
|
||||
}
|
||||
|
||||
/// Paginated list of workrooms.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkroomListResponse {
|
||||
pub workrooms: Vec<WorkroomSummary>,
|
||||
}
|
||||
|
||||
/// Response from the `/workroom/resolve` endpoint.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct WorkroomResolveResponse {
|
||||
pub link: WorkroomLink,
|
||||
pub thread_title: Option<String>,
|
||||
pub external_ref: Option<ExternalThreadRef>,
|
||||
pub recent_events: Vec<WorkroomEvent>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn workroom_id_new_is_stable() {
|
||||
let id = WorkroomId::new();
|
||||
assert!(id.0.starts_with("wr_"));
|
||||
assert_eq!(id.0.len(), 35); // "wr_" + 32 hex chars
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workroom_link_parse_workroom_only() {
|
||||
let link = WorkroomLink::parse("codewhale://workroom/wr_abc123def456").unwrap();
|
||||
assert_eq!(link.workroom_id.0, "wr_abc123def456");
|
||||
assert!(link.thread_id.is_none());
|
||||
assert!(link.event_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workroom_link_parse_with_thread() {
|
||||
let link = WorkroomLink::parse("codewhale://workroom/wr_abc/thread/thr_xyz").unwrap();
|
||||
assert_eq!(link.workroom_id.0, "wr_abc");
|
||||
assert_eq!(link.thread_id.as_deref(), Some("thr_xyz"));
|
||||
assert!(link.event_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workroom_link_parse_with_event() {
|
||||
let link = WorkroomLink::parse("codewhale://workroom/wr_abc/event/evt_789").unwrap();
|
||||
assert_eq!(link.workroom_id.0, "wr_abc");
|
||||
assert_eq!(link.event_id.as_deref(), Some("evt_789"));
|
||||
assert!(link.thread_id.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workroom_link_roundtrip() {
|
||||
let original = "codewhale://workroom/wr_abc/thread/thr_x/event/evt_y";
|
||||
let parsed = WorkroomLink::parse(original).unwrap();
|
||||
assert_eq!(parsed.to_url(), original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workroom_link_reject_bad_prefix() {
|
||||
assert!(WorkroomLink::parse("http://workroom/wr_abc").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://not-workroom/wr_abc").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workroom_link_rejects_malformed_paths() {
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/abc").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/wr_").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/wr_abc/thread").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/wr_abc/thread/").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/wr_abc/unknown/x").is_none());
|
||||
assert!(WorkroomLink::parse("codewhale://workroom/wr_abc/event/evt/x").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn external_thread_ref_serde_roundtrip() {
|
||||
let issue = ExternalThreadRef::GitHubIssue {
|
||||
owner: "Hmbown".into(),
|
||||
repo: "CodeWhale".into(),
|
||||
number: 3209,
|
||||
};
|
||||
let json = serde_json::to_string(&issue).unwrap();
|
||||
let back: ExternalThreadRef = serde_json::from_str(&json).unwrap();
|
||||
assert!(matches!(back, ExternalThreadRef::GitHubIssue { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_attribution_serde_roundtrip() {
|
||||
let attr = AgentAttribution {
|
||||
provider: "deepseek".into(),
|
||||
model: "deepseek-v4-pro".into(),
|
||||
agent_id: "sub_agent_1".into(),
|
||||
};
|
||||
let json = serde_json::to_string(&attr).unwrap();
|
||||
let back: AgentAttribution = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back.provider, "deepseek");
|
||||
assert_eq!(back.model, "deepseek-v4-pro");
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,21 @@
|
||||
# RFC: Codewhale Workrooms — Chat-native Threaded Agent Work
|
||||
|
||||
**Issue:** #3209
|
||||
**Status:** Future RFC — Phase 1 shipped (protocol types + link parser in
|
||||
`crates/protocol/src/workroom.rs`); Phase 2 (store, app-server endpoints,
|
||||
`resolve_workroom_link` tool, TUI inbox) not started
|
||||
**Status:** Future RFC — the Workroom wire shape and link parser are defined
|
||||
once, outside this repository: `cwc_core::workroom` and
|
||||
`packages/contracts/src/workroom.js` in codewhale-contracts (authority:
|
||||
codewhale-platform `crates/cwc-core`), which a Project's room serves. The
|
||||
earlier copy in `crates/protocol/src/workroom.rs` had no consumers and was
|
||||
removed. Phase 2 (store, app-server endpoints, `resolve_workroom_link` tool,
|
||||
TUI inbox) not started
|
||||
**Date:** 2026-06-17 (status refreshed 2026-07-15)
|
||||
**Target:** post-v0.9.0; the release label is a maintainer decision, not part
|
||||
of this design
|
||||
|
||||
This document is design scaffolding. As of v0.9.0 the tree carries the
|
||||
shared protocol types and link parser only; runtime endpoints, mobile UI
|
||||
integration, persistent state, and model-visible tools remain follow-up work.
|
||||
This document is design scaffolding. This tree carries none of it: the
|
||||
protocol types and link parser live in codewhale-contracts (see Status), and
|
||||
runtime endpoints, mobile UI integration, persistent state, and model-visible
|
||||
tools remain follow-up work.
|
||||
|
||||
## 1. Problem
|
||||
|
||||
|
||||
Reference in new issue
Block a user