Skip to content

Release 0.3.0 — Channel projection + chat-of-record + merge-hand contract - #56

Merged
daxxsyntax merged 4 commits into
mainfrom
release/0.3.0
Apr 20, 2026
Merged

Release 0.3.0 — Channel projection + chat-of-record + merge-hand contract#56
daxxsyntax merged 4 commits into
mainfrom
release/0.3.0

Conversation

@daxxsyntax

Copy link
Copy Markdown
Owner

Summary

Three epics shipped on release/0.3.0:

Merge commits

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 in preset_unread_count, preset-filter clear semantics, default-channel status filter, mention-override gated on explicit nick, sender-label prefers metadata.actor_id, SelectTab IRC refresh) and known-incomplete items tracked as follow-up sparks (N+1 in unread counts + precondition checker, JumpToTail scroll wiring, Task-chain for initial unread badge load).

Known tech debt from this release

  1. 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.

  2. Base-branch stacking failed on merge-hand-contract's Epic-PR child. sp-9f6b0c96 (Epic PR lifecycle) branched off main without stacking onto sp-6b261ad0 (Sub-PR merge flow), producing an unresolvable-by-automation merge conflict. The blocks bond 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 in resolve_hand_base_ref.

  3. Merger defaults to main for the base branch (recurring). Mergers on #53, #54, #55 all opened against main by default instead of release/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.

  4. Format check uses stable rustfmt but .rustfmt.toml has unstable features. Local nightly cargo fmt produces a different output than CI's stable cargo fmt --check. Surfaced on PR crew cr-f9bb4626 v2: 0.3.0 merge-hand-contract — manual integration with conflict resolve #55 and fixed in 058d5e9 with another fmt pass.

Test plan

  • ryve init a fresh workshop. ryve post --channel '#atlas' 'hello' then ryve channel tail --channel '#atlas' --limit 5 — the post round-trips through irc_messages.
  • Claim a spark under an epic, then try ryve assign close <session> <spark> without posting. Refuse with a clear message. Post once, retry — closes.
  • Spawn a Hand on an epic child. Kill its session mid-task, re-spawn. The new Hand's prompt contains the prior session's posts as context.
  • Open the workshop UI. Click the IRC view tab. See the projection panel with filter chips, preset picker, unread badges. Create a preset, switch to another channel and back — preset state persists.
  • Run ryve head spawn <epic> --archetype build --agent claude on an epic with at least 2 blocks-bonded children that have both landed. The MergeHand contract's precondition checker admits the spawn when all children are Approved.

🤖 Generated with Claude Code

… 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.
Copilot AI review requested due to automatic review settings April 20, 2026 06:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 close gate), 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.

Comment thread ipc/src/lifecycle.rs
Comment on lines +428 to +437
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?;

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +25 to +27
-- 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.

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
-- 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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread ipc/src/chat_of_record.rs Outdated
Comment on lines +14 to +16
//! 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.

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
//! 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.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@daxxsyntax
daxxsyntax merged commit bf0d6cd into main Apr 20, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants