Release 0.3.0 — Channel projection + chat-of-record + merge-hand contract - #56
Conversation
… sudden-death recovery (#54) Six children landed: `ryve-d9523f48` (foundation: post + channel tail CLI primitives), `ryve-850c0242` (#atlas well-known channel + Atlas boot/shutdown posts), `ryve-9e6ef4e8` (`ryve assign close` mandatory-post enforcement), `ryve-b6d9a53c` (boot-prepend recent channel context for re-claimed sparks), `ryve-979b1ec3` (MCP `chat.post` + `chat.tail` tools), `ryve-73fd1c5f` (Atlas/Head/Hand prompt updates referencing the boundaries + tail tool). PR #54 Copilot review (7 comments) all addressed in 39b6f9d: c1+c4+c6 — `--author` placeholder renamed from `<actor_id>` to `<session_id>` (the value is an `agent_sessions.id`, not Ryve's separate actor_id concept). User-facing copy fixed in cli help, RYVE.md, HEAD_ARCHETYPES.md. c2 — TailFilter.limit doc now describes the actual reject-on-invalid behaviour (was "clamped"). c3 — REAL BUG: `count_posts_since_claim` extended via parent_child bond fallback so Hands on non-epic sparks who post to the parent epic's channel are correctly counted (regression test included). c5 — agent_prompts.rs: `spark close` mention removed (only `assign close` is gated). c7 — RYVE.md handoff row references `ryve assign close` not `assign release`. Parent epic: ryve-12f09190.
…+ presets (#53) Two original children: `ryve-21247afb` (5-axis projection query module + FTS + mentions override + preset persistence) and `ryve-5466c372` (UI panel — scrollable live view + preset picker + filter chips + search + unread badges). Plus the Head's wave-2 expansions: `ryve-a3cde279` (data layer FTS5+metadata), `ryve-1e481704` (preset persistence), `ryve-18116600` (UI base), `ryve-37faabbe` (preset picker + unread + FTS UI). PR #53 Copilot review (13 comments) all addressed in 0da0176 + merge-resolve 01aa47a: Real bugs fixed: c2 (preset_unread_count missing validate_fts), c6 (PresetFilters can't clear epic_id), c9 (default channel picked closed epics), c10 (mention override always-on for default config), c12 (sender label was session_id not actor), c11 (SelectTab didn't refresh IRC view). Doc fixes: c1 (limit doc), c3+c7 (unread arithmetic), c8 (Option<u64> doc). Documented-but-not-fixed (follow-up sparks): c4 (initial unread badges blank for one poll cycle, needs Task chaining), c5 (load_unread_counts N+1, needs grouped query), c13 (JumpToTail viewport doesn't move, needs scrollable::Id wiring + Task return from handler). Migration file deliberately not modified (immutable applied migrations house rule); doc explanation moved into channel_projection.rs source. Merge resolution 01aa47a: ipc/src/lib.rs conflict (both branches added a new module on the same line) — kept both modules. Parent epic: ryve-06816a07.
…ith bond-discipline conflict resolve (#55) Six children landed: merge_hand role + prompt composer skeleton (sp-10c8baee), precondition checker (sp-5bd7749b), sub-PR → Epic merge flow (sp-6b261ad0), conflict handoff via transition validator (sp-086a4432), Epic PR lifecycle + Epic→main merge (sp-9f6b0c96), E2E test (sp-51daf9b1). The original automated Merger (Hand 9ef82731) hit a bond-discipline conflict in src/agent_prompts.rs::compose_merge_hand_prompt: sp-9f6b0c96 branched off main without base-stacking onto sp-6b261ad0, so both rewrote the same function body independently. Manually integrated by combining both workflows into a single coherent MergeHand prompt (WORKFLOW 1-8 sub-PR merge loop + EPIC PR LIFECYCLE A-G Epic→main + combined HARD RULES). PR #55 Copilot review (4 comments) addressed: c1 (7db6e58) — Step 8 vs EPIC PR LIFECYCLE contradiction resolved. c2 (7db6e58) — E2E test module doc reworded to match the raw-SQL approach. c3 (7db6e58) — N+1 in check_merge_preconditions documented + tracked as follow-up perf spark. c4 (7db6e58) — `status.kind() == "fail"` replaced with typed `status.is_fail()`. c4 follow-up (8b52d08) — `#[allow(dead_code)]` on the now-unused-in-release `kind()` method so `-D warnings` stays clean. Parent epic: ryve-476ef264.
There was a problem hiding this comment.
Pull request overview
Release 0.3.0 integration delivering chat-of-record (durable IRC-backed posting + tailing + close gate), channel projection (filtered IRC views + presets + unread), and the MergeHand contract (new role + locked transitions + E2E coverage) across the ipc, data, UI, and CLI layers.
Changes:
- Add chat-of-record primitives (
ipc::chat_of_record), CLI commands (ryve post,ryve channel tail,ryve assign closegate), and MCP wrappers (chat.post,chat.tail). - Introduce channel projection query engine + persistence (presets/unread) and a new UI IRC projection tab with periodic refresh.
- Implement MergeHand transition constraints (conflict handoff + merge-only “Merged” transition) and add end-to-end tests for the happy path.
Reviewed changes
Copilot reviewed 32 out of 32 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/merge_hand_e2e.rs | End-to-end test covering MergeHand epic→main lifecycle invariants. |
| tests/assign_close_cli.rs | CLI integration test for mandatory chat-of-record posting gate on assign close. |
| src/workshop.rs | Adds per-tab IRC view state and helpers to open/focus IRC projection tabs. |
| src/screen/mod.rs | Registers the new irc_view screen module. |
| src/screen/bench.rs | Adds TabKind::IrcView and a menu action to open the IRC view. |
| src/panel_state.rs | Re-exports IrcViewState via panel_state façade. |
| src/mcp/mod.rs | Introduces MCP registry/dispatcher and tool descriptors. |
| src/mcp/chat_tools.rs | Implements chat.post/chat.tail MCP tools with JSON schema + tests. |
| src/main.rs | Wires the new mcp module into the bin crate. |
| src/hand_archetypes.rs | Adds MergeHand archetype id + write-capable tool policy + tests. |
| src/cli.rs | Adds post, channel tail, mcp list/call, and assign close gate plumbing. |
| src/app.rs | Routes IRC view messages, refresh scheduling, and tab focus refresh behavior. |
| ipc/tests/chat_of_record.rs | Integration tests for post_message, tail, and count_posts_since_claim. |
| ipc/tests/channel_projection.rs | Integration tests for projection axes, FTS, mentions override, presets + unread. |
| ipc/tests/atlas_channel.rs | Integration tests for #atlas persistence + boot/shutdown seat logging. |
| ipc/src/lifecycle.rs | Boots #atlas channel + seat logging and persists shutdown marker. |
| ipc/src/lib.rs | Exposes new channel_projection and chat_of_record modules. |
| ipc/src/chat_of_record.rs | Adds durable post/tail primitives and mandatory-post counting helper. |
| ipc/src/channel_projection.rs | Implements filtered message projection, presets CRUD, and unread counts. |
| ipc/src/channel_manager.rs | Adds #atlas channel constants + sentinel spark creation/join helpers. |
| ipc/Cargo.toml | Adds uuid dependency for v4 ids. |
| docs/HEAD_ARCHETYPES.md | Documents chat-of-record boundaries and the tool-gated close behavior. |
| data/tests/transition_phase.rs | Adds tests for conflict handoff + merge-only “Merged” transition behavior. |
| data/tests/github_applier.rs | Adjusts lifecycle expectations due to new MergeHand-only transition edge. |
| data/src/sparks/transition.rs | Adds conflict-handoff rule, reason validation, and override lockdown flags. |
| data/src/sparks/error.rs | Adds InvalidReason transition error for reason-gated edges. |
| data/migrations/023_projection_presets.sql | Adds projection_presets table + indexes for saved projection filters. |
| RYVE.md | Documents chat-of-record workflow and CLI usage. |
| Cargo.toml | Enables sqlx migrate feature for #[sqlx::test(migrations=...)] usage. |
| async fn detect_active_seat_claim(pool: &SqlitePool) -> Result<bool, chat_of_record::ChatError> { | ||
| // Use the chat_of_record tail primitive so the seat scan stays on | ||
| // the same indexed path as every other #atlas reader. The | ||
| // chat-of-record epic caps sane windows at TAIL_MAX_LIMIT; for a | ||
| // single workshop's boot history that's more than enough headroom. | ||
| let rows = chat_of_record::tail( | ||
| pool, | ||
| TailFilter::for_channel(ATLAS_CHANNEL).with_limit(TAIL_MAX_LIMIT), | ||
| ) | ||
| .await?; |
There was a problem hiding this comment.
detect_active_seat_claim only inspects the last TAIL_MAX_LIMIT rows of #atlas via chat_of_record::tail. Since #atlas is intended to be a long-lived chat-of-record channel (boot + routing + handoffs), it can easily exceed 1000 messages; if an unreleased online, seat claim marker scrolls out of this window, a new Atlas could incorrectly boot as Claim instead of Follower. Consider querying only the seat-marker rows (e.g. raw_text LIKE 'atlas % online, seat %' OR 'atlas % offline, seat released') and scanning all of those, or doing a targeted SQL existence check for an unreleased claim without a hard tail cap.
There was a problem hiding this comment.
Addressed in 2d3846a — real bug. Switched from chat_of_record::tail(... TAIL_MAX_LIMIT) (last 1000 rows of ALL #atlas traffic) to a filtered SQL query that only reads rows starting with 'atlas ' (the shape the parsers recognise), bounded to 5000 rows. For plausible workshop lifetimes that covers every seat event in history; the bound is a belt-and-braces guard.
| -- had observed while this preset was active. Unread count is derivable as | ||
| -- `max(irc_messages.id matching preset filters) - last_seen_message_id` | ||
| -- without touching this table beyond a read. |
There was a problem hiding this comment.
The migration comment states unread count is derivable as max(irc_messages.id matching preset filters) - last_seen_message_id, but ipc::channel_projection::preset_unread_count explicitly avoids that arithmetic (uses COUNT(*) WHERE id > last_seen) because ids aren’t gap-free within a filter slice. Please update this comment to match the actual unread-count contract so future readers don’t reintroduce the incorrect assumption.
| -- had observed while this preset was active. Unread count is derivable as | |
| -- `max(irc_messages.id matching preset filters) - last_seen_message_id` | |
| -- without touching this table beyond a read. | |
| -- had observed while this preset was active. Unread count is derived by | |
| -- counting `irc_messages` rows that both match the preset filters and | |
| -- have `id > last_seen_message_id`; it is not safe to subtract IDs | |
| -- because matching message IDs need not be contiguous within a filtered | |
| -- projection. |
There was a problem hiding this comment.
Not fixed in this PR — data/migrations/*.sql files are sqlx migrations tracked by checksum. Even a comment-only edit breaks the migration tracker on every existing workshop (house rule documented in user memory). Accurate description lives in ipc::channel_projection::preset_unread_count's doc, which is what callers actually read. We could ship an 024_*.sql corrigendum migration if the outdated comment on 023 causes real confusion, but for now the source-of-truth doc in the source file is the right pointer.
| //! primary key. The database write is the durability contract; IRC wire | ||
| //! delivery via the outbox relay is best-effort and may no-op when no | ||
| //! relay is running. |
There was a problem hiding this comment.
Module docs say IRC wire delivery is “best-effort … via the outbox relay”, but post_message never enqueues an outbox row and there’s no other code path here that sends the message to the IRC server. Either implement actual best-effort wire emission (e.g. via outbox or an injected IrcClient) or update the docs to state clearly that this module only persists to irc_messages and wire delivery (if any) is handled elsewhere.
| //! primary key. The database write is the durability contract; IRC wire | |
| //! delivery via the outbox relay is best-effort and may no-op when no | |
| //! relay is running. | |
| //! primary key. The database write is the durability contract provided | |
| //! by this module; IRC wire delivery, if any, is handled elsewhere and | |
| //! is not performed here. |
There was a problem hiding this comment.
Addressed in 2d3846a — doc reworded. post_message writes the durable row and nothing else; cross-agent coordination happens through ryve channel tail reading the DB. IRC-wire emission for chat-of-record posts is flagged as a 0.4.0 candidate. Prior wording ("best-effort via outbox relay") was aspirational and inconsistent with the implementation.
c1 (ipc/src/lifecycle.rs::detect_active_seat_claim) — REAL BUG
Earlier implementation read the last 1000 rows of #atlas via
`chat_of_record::tail`. On a long-running workshop #atlas
accumulates routing chatter beyond 1000 rows; a still-unreleased
seat-claim marker can scroll out of that window and a fresh
Atlas boots as Primary while the prior seat-holder is still
alive. Replaced with a filtered SQL query that only reads rows
with `raw_text LIKE 'atlas %'` (the shape the parsers
recognise), bounded to 5000 rows — effectively every seat event
in the channel's history for plausible workshop lifetimes.
Dropped the unused `TAIL_MAX_LIMIT` / `TailFilter` imports.
c2 (data/migrations/023_projection_presets.sql:27) — NOT FIXED
The migration comment describes the old arithmetic form of the
unread count. Applied sqlx migrations are checksum-tracked; even
comment-only edits break the migration tracker (house rule). The
accurate description lives in `channel_projection.rs::preset_unread_count`
which callers actually read. Documented in the reply.
c3 (ipc/src/chat_of_record.rs module doc) — REAL INACCURACY
Doc claimed IRC wire delivery was "best-effort via the outbox
relay", but `post_message` never enqueues an outbox row. Posts
are durable in `irc_messages` and readable via `tail` — but they
do NOT land on the actual IRC wire, so subscribers on the IRC
server never see them. Reworded to describe this accurately and
flag IRC-wire emission as a 0.4.0 candidate.
Local verification:
- cargo build --bin ryve clean (one unused-import warning
fixed inline)
- cargo clippy --workspace -- -D warnings clean
- cargo test -p ipc detect_active_seat_claim — no dedicated
test to run; helper is exercised indirectly via lifecycle
boot path
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Three epics shipped on release/0.3.0:
ryve-12f09190):ryve post+ryve channel tailCLI primitives,#atlaswell-known workshop channel,ryve assign closegate refusing zero-post closures, boot-prepend of recent channel context inhand_spawn, MCPchat.post+chat.tailwrappers, Atlas/Head/Hand prompt updates referencing the mandatory posting boundaries.ryve-06816a07): 5-axis query module (epic/spark/assignment/PR/actor) with FTS5 search + mention override, filter preset persistence with unread tracking, live UI panel with preset picker + search + unread badges.ryve-476ef264): newmerge_handrole with prompt composer, precondition checker (all Approved + no conflicts + CI green + zero Stuck), Sub-PR → Epic merge flow in Assignment-creation order withgit merge --no-ff, conflict handoff via transition validator (Approved → Rejectedreason=conflict), Epic PR lifecycle driving Epic → main merge withreviewDecision+statusCheckRollupgates andmark_assignment_mergedpinned tomerge_handrole, E2E test covering the full happy path.Merge commits
cca8420chat-of-record (PR Chat-of-record: mandatory posting + #atlas history + sudden-death recovery reader [sp-12f09190] #54)f07c870channel projection (PR crew cr-43e1e251: channel projection — query module + UI panel [sp-06816a07] #53)0a24ae2merge-hand contract (PR crew cr-f9bb4626 v2: 0.3.0 merge-hand-contract — manual integration with conflict resolve #55)Copilot review addressed across the three PRs
24 comments total (7 on #54, 13 on #53, 4 on #55) covering real correctness bugs (parent-epic fallback in
count_posts_since_claim, FTS validation inpreset_unread_count, preset-filter clear semantics, default-channel status filter, mention-override gated on explicit nick, sender-label prefersmetadata.actor_id,SelectTabIRC refresh) and known-incomplete items tracked as follow-up sparks (N+1 in unread counts + precondition checker,JumpToTailscroll wiring, Task-chain for initial unread badge load).Known tech debt from this release
Dead-Head-after-dispatch pattern. All three 0.3.0 Heads exited silently after dispatching their wave-1 / wave-2 children (claiming a cron poll that was never created). Manual intervention pushed Hand branches + spawned Mergers. Atlas archetype should either (a) genuinely set up a cron, or (b) block on child closures instead of pretending it set up a watcher.
Base-branch stacking failed on
merge-hand-contract's Epic-PR child.sp-9f6b0c96(Epic PR lifecycle) branched off main without stacking ontosp-6b261ad0(Sub-PR merge flow), producing an unresolvable-by-automation merge conflict. Theblocksbond was declared but the Hand spawn resolver didn't find a branch tip to cut from at the moment of spawn. Needs investigation — likely a timing/race inresolve_hand_base_ref.Merger defaults to
mainfor the base branch (recurring). Mergers on#53,#54,#55all opened againstmainby default instead ofrelease/0.3.0. Retargeted by hand each time. Same issue as 0.2.1; still unfixed. The Merger prompt needs to learn about the release branch from the release row or parent epic's release bond.Format check uses stable rustfmt but
.rustfmt.tomlhas unstable features. Local nightlycargo fmtproduces a different output than CI's stablecargo fmt --check. Surfaced on PR crew cr-f9bb4626 v2: 0.3.0 merge-hand-contract — manual integration with conflict resolve #55 and fixed in058d5e9with another fmt pass.Test plan
ryve inita fresh workshop.ryve post --channel '#atlas' 'hello'thenryve channel tail --channel '#atlas' --limit 5— the post round-trips throughirc_messages.ryve assign close <session> <spark>without posting. Refuse with a clear message. Post once, retry — closes.ryve head spawn <epic> --archetype build --agent claudeon an epic with at least 2blocks-bonded children that have both landed. The MergeHand contract's precondition checker admits the spawn when all children are Approved.🤖 Generated with Claude Code