From b8337c43a2a683e37e17a73384cb009d234f5c47 Mon Sep 17 00:00:00 2001 From: Eric Garcia Date: Tue, 3 Feb 2026 10:08:00 -0500 Subject: [PATCH] feat: RFC 0057 Judge Convergence Discipline (Phases 1-7) Fixes convergence discipline with four core changes: 1. Velocity redefined: velocity = open_tensions + new_perspectives (work remaining, not score delta) 2. Unanimous convergence: 100% of experts must signal [MOVE:CONVERGE] 3. Both conditions required: velocity=0 AND converge=100% 4. Dialogues persist: stored in .blue/dialogues/-/ Database schema updates: - Added score component columns (W/C/T/R breakdown) - Added convergence tracking columns - Created alignment_convergence_signals table - Created alignment_scoreboard view MCP tool updates: - round_register accepts new metrics - round_context returns convergence status - verdict_register gates on convergence criteria - dialogue_create outputs to .blue/dialogues/ - dialogue_export includes full scoreboard Judge protocol updated with new scoreboard format and max_rounds=10. Co-Authored-By: Claude Opus 4.5 --- .blue/dialogues/.gitkeep | 7 + ...0014-alignment-dialogue-agents.accepted.md | 43 +- ...7-judge-convergence-discipline.approved.md | 888 ++++++++++++++++++ crates/blue-core/src/alignment_db.rs | 474 +++++++++- crates/blue-mcp/src/handlers/dialogue.rs | 215 ++++- skills/alignment-play/SKILL.md | 96 +- 6 files changed, 1683 insertions(+), 40 deletions(-) create mode 100644 .blue/dialogues/.gitkeep create mode 100644 .blue/docs/rfcs/0057-judge-convergence-discipline.approved.md diff --git a/.blue/dialogues/.gitkeep b/.blue/dialogues/.gitkeep new file mode 100644 index 0000000..3373e1f --- /dev/null +++ b/.blue/dialogues/.gitkeep @@ -0,0 +1,7 @@ +# Blue Dialogues + +This directory stores alignment dialogue artifacts. + +Naming convention: `YYYY-MM-DDTHHMMZ-/` + +Example: `2026-02-03T1423Z-nvidia-investment/` diff --git a/.blue/docs/adrs/0014-alignment-dialogue-agents.accepted.md b/.blue/docs/adrs/0014-alignment-dialogue-agents.accepted.md index 933b0b3..0a9fa6d 100644 --- a/.blue/docs/adrs/0014-alignment-dialogue-agents.accepted.md +++ b/.blue/docs/adrs/0014-alignment-dialogue-agents.accepted.md @@ -125,16 +125,22 @@ Unbounded scoring: - Reflects reality: there's always more ALIGNMENT to achieve - Makes velocity meaningful: +2 vs +20 tells you something -### ALIGNMENT Velocity +### ALIGNMENT Velocity (Updated: RFC 0057) -The dialogue tracks cumulative ALIGNMENT: +The dialogue tracks cumulative ALIGNMENT and convergence metrics: ``` Total ALIGNMENT = Ξ£(all turn scores) -ALIGNMENT Velocity = score(round N) - score(round N-1) +Velocity = open_tensions + new_perspectives ``` -When **ALIGNMENT Velocity approaches zero**, the dialogue is converging. New rounds aren't adding perspectives. Time to finalize. +**Velocity** measures "work remaining" β€” the number of unresolved tensions plus new perspectives surfaced this round. When **Velocity = 0**, all tensions are resolved and no new perspectives are emerging. + +**Convergence requires:** +- Velocity = 0 (no work remaining) +- 100% of experts signal `[MOVE:CONVERGE]` + +This is stricter than the original score-delta definition, ensuring dialogues don't converge prematurely with open tensions. ## The Agents @@ -237,11 +243,10 @@ The πŸ’™ loves them all. Wants them all to shine. Helps them find the most align β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ EACH ROUND: Spawn N agents IN PARALLEL β”‚ -β”‚ LOOP until: β”‚ -β”‚ - ALIGNMENT Plateau (velocity β‰ˆ 0) β”‚ -β”‚ - All tensions resolved β”‚ -β”‚ - πŸ’™ declares convergence β”‚ -β”‚ - Max rounds reached (safety valve) β”‚ +β”‚ LOOP until (RFC 0057): β”‚ +β”‚ - Velocity = 0 (open_tensions + new_perspectives) β”‚ +β”‚ - 100% experts signal [MOVE:CONVERGE] β”‚ +β”‚ - Max rounds reached (10, safety valve) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` @@ -406,15 +411,21 @@ The N-agent parallel architecture provides: - No race conditions (all write to separate outputs, Judge merges) - Claude Code's Task tool supports parallel spawning natively -## Convergence Criteria +## Convergence Criteria (Updated: RFC 0057) -The πŸ’™ declares convergence when ANY of: +The πŸ’™ declares convergence when **ALL** of: -1. **ALIGNMENT Plateau** - Velocity β‰ˆ 0 for two consecutive rounds (across all N agents) -2. **Full Coverage** - Perspectives Inventory has no βœ— items (all integrated or consciously deferred) -3. **Zero Tensions** - All `[TENSION]` markers have matching `[RESOLVED]` -4. **Mutual Recognition** - Majority of 🧁s state they believe ALIGNMENT has been reached -5. **Max Rounds** - Safety valve (default: 5 rounds) +1. **Velocity = 0** - `open_tensions + new_perspectives == 0` ("work remaining" is zero) +2. **Unanimous Recognition** - 100% of 🧁s signal `[MOVE:CONVERGE]` + +Plus safety valve: +3. **Max Rounds** - 10 rounds (forces convergence with warning) + +**Key changes from original (RFC 0057):** +- Velocity redefined as `open_tensions + new_perspectives` (not score delta) +- Unanimous expert agreement required (not majority) +- Both conditions must be true (AND, not OR) +- Max rounds increased from 5 to 10 The πŸ’™ can also **extend** the dialogue if it sees unincorporated perspectives that no 🧁 has surfaced. diff --git a/.blue/docs/rfcs/0057-judge-convergence-discipline.approved.md b/.blue/docs/rfcs/0057-judge-convergence-discipline.approved.md new file mode 100644 index 0000000..d74b6b8 --- /dev/null +++ b/.blue/docs/rfcs/0057-judge-convergence-discipline.approved.md @@ -0,0 +1,888 @@ +# RFC 0057: Judge Convergence Discipline + +**Status:** Approved +**Author:** Eric +**Created:** 2026-02-02 +**Relates To:** ADR 0014, RFC 0050, RFC 0051 + +## Overview + +Fixes convergence discipline with four changes: +1. **Redefine Velocity** β€” `velocity = open_tensions + new_perspectives` (work remaining, not score delta) +2. **Require Unanimous Convergence** β€” 100% of experts must signal `[MOVE:CONVERGE]` +3. **Both Conditions Required** β€” Convergence needs velocity = 0 AND unanimous agreement +4. **Persist Dialogues** β€” Store in `.blue/dialogues/` not `/tmp` for visibility + +Default max rounds: **10** + +## Problem Statement + +### Bug 1: Tension Gate Bypass + +In default mode (`rotation: graduated`), the Judge sometimes jumps directly to RFC creation without first resolving open tensions. The Judge Protocol contains `tension_resolution_gate: true`, but the SKILL.md lacks explicit enforcement instructions, leading to premature convergence declarations. + +**Observed Behavior:** +- Judge declares convergence +- Judge proposes/creates RFC +- Open tensions remain unaddressed + +**Expected Behavior:** +- Judge checks open tensions +- If tensions exist β†’ continue dialogue with targeted expertise +- Only when tensions resolved β†’ declare convergence β†’ create RFC + +### Issue 2: Velocity Misdefined + +Velocity was defined as score delta between rounds β€” a derivative metric meant to detect "diminishing returns." This is problematic: + +- Threshold tuning (what's "low enough"?) +- Misreporting (showing 0 when actual delta was 89) +- Derivative metric when primary signals are clearer + +**Proposal:** Redefine velocity as "work remaining": + +``` +Velocity = open_tensions + new_perspectives +``` + +When velocity = 0: +- No open tensions (conflicts resolved) +- No new perspectives surfaced (coverage complete) + +This is a direct measurement, not a derivative. + +## Proposed Solution + +### 1. Redefined Convergence Criteria + +Convergence requires ALL of: + +| Condition | How Measured | +|-----------|--------------| +| **Velocity = 0** | `open_tensions + new_perspectives == 0` | +| **Unanimous Recognition** | 100% of experts signal `[MOVE:CONVERGE]` | + +Plus safety valve: +| **Max Rounds** | 10 rounds (default) β€” forces convergence even if conditions not met | + +**Both conditions must be true** (except max rounds override). This is stricter than before: +- Can't converge with open tensions +- Can't converge while new perspectives are still emerging +- Can't converge without unanimous expert agreement + +### 2. Explicit Tension Gate in Judge Workflow + +Add a mandatory "Tension Gate Check" after each round. + +**After Round N completes:** + +``` +TENSION_GATE: +1. Query open tensions from round context +2. IF open_tension_count > 0: + a. Analyze each open tension + b. For each tension: + - Does current panel have expertise to resolve? β†’ retain expert, assign directive + - Does pool have relevant expert? β†’ pull from pool + - Neither? β†’ create targeted expert + c. Evolve panel via `blue_dialogue_evolve_panel` + d. Spawn next round with tension-focused prompts + e. RETURN TO TENSION_GATE after round completes +3. IF open_tension_count == 0: + β†’ Proceed to convergence declaration +``` + +### 3. Round Summary Artifacts + +**Mandatory in `round-N.summary.md`:** + +```markdown +## Velocity Components + +### Open Tensions: 1 +| ID | Label | Status | Owner | Resolution Path | +|----|-------|--------|-------|-----------------| +| T0001 | Income mandate conflict | OPEN | Muffin | Covered call analysis next round | +| T0002 | Concentration risk | RESOLVED | Scone | Diversification accepted | + +### New Perspectives This Round: 2 +| ID | Label | Contributor | +|----|-------|-------------| +| P0201 | Options overlay strategy | Cupcake | +| P0202 | Tax-loss harvesting window | Donut | + +### Convergence Signals: 3/6 (50%) +| Expert | Signal | +|--------|--------| +| Muffin | `[MOVE:CONVERGE]` βœ“ | +| Cupcake | `[MOVE:CONVERGE]` βœ“ | +| Scone | `[MOVE:CONVERGE]` βœ“ | +| Donut | β€” | +| Eclair | β€” | +| Brioche | β€” | + +## Velocity: 3 (1 tension + 2 perspectives) +## Converge %: 50% +## Convergence Blocked: Yes (velocity > 0, converge < 100%) +``` + +The Judge MUST write this after every round. Convergence requires velocity = 0 AND converge % = 100%. + +### 4. Convergence Decision Tree + +``` + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Round Complete β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Calculate: β”‚ + β”‚ - ALIGNMENT β”‚ + β”‚ - Velocity β”‚ + β”‚ - Converge % β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Max Rounds? │──YES──► FORCE CONVERGENCE + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (with warning) + β”‚ NO + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Velocity = 0? │──NO───► NEXT ROUND + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (resolve tensions, + β”‚ YES surface perspectives) + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ 100% Converge? │──NO───► NEXT ROUND + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ (experts not aligned) + β”‚ YES + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ DECLARE β”‚ + β”‚ CONVERGENCE β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Create RFC or β”‚ + β”‚ Final Verdict β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +Both conditions must pass: `velocity == 0` AND `converge_percent == 100%`. + +### 5. Tension-Focused Prompts + +When spawning a round specifically to address tensions, the Judge SHOULD include tension directives in the prompts: + +```markdown +## Directive for This Round + +**Priority Tensions:** +- T0001: Income mandate conflict - You have been retained/summoned to address this +- Action: Propose concrete resolution or explain why tension is acceptable + +**Expected Output:** +- Either `[RE:RESOLVE T0001]` with supporting analysis +- Or `[RE:ADDRESS T0001]` explaining why resolution is blocked +``` + +### 6. Acceptable Unresolved Tensions + +Some tensions may be legitimately unresolvable (fundamental tradeoffs). The Judge MAY declare convergence with open tensions IF: + +1. The tension is explicitly marked as `[ACCEPTED UNRESOLVED]` +2. The final verdict acknowledges the unresolved tension +3. The RFC/recommendation accounts for the tradeoff + +**Syntax in summary:** + +```markdown +| T0003 | Growth vs. income | ACCEPTED UNRESOLVED | β€” | Fundamental tradeoff, mitigated by position sizing | +``` + +This is different from "forgot to resolve" β€” it's a conscious decision documented in the record. + +### 7. Scoreboard Format + +Track both ALIGNMENT components and convergence metrics. + +**New format:** + +```markdown +## Scoreboard + +| Round | W | C | T | R | Score | Open Tensions | New Perspectives | Velocity | Converge % | +|-------|---|---|---|---|-------|---------------|------------------|----------|------------| +| 0 | 45| 30| 25| 25| 125 | 3 | 8 | 11 | 0% | +| 1 | 32| 22| 18| 17| 89 | 1 | 2 | 3 | 50% | +| 2 | 18| 12| 8 | 7 | 45 | 0 | 0 | 0 | 100% | + +**Total ALIGNMENT:** 259 (W:95 C:64 T:51 R:49) +**Max Rounds:** 10 +**Convergence:** βœ“ (velocity=0, unanimous) +``` + +**ALIGNMENT Components:** +- **W** = Wisdom (perspectives integrated, synthesis quality) +- **C** = Consistency (follows patterns, internally coherent) +- **T** = Truth (grounded in reality, no contradictions) +- **R** = Relationships (connections to other artifacts, graph completeness) + +**Convergence Metrics:** +- **Velocity** = Open Tensions + New Perspectives ("work remaining") +- **Converge %** = percentage of experts who signaled `[MOVE:CONVERGE]` + +When velocity hits 0 and converge % hits 100%, the dialogue is complete. + +#### Convergence Summary Template + +When convergence is achieved, the Judge outputs a final summary: + +``` +100% CONVERGENCE ACHIEVED + +Final Dialogue Summary +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Metric β”‚ Value β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Rounds β”‚ 3 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Total ALIGNMENT β”‚ 289 β”‚ +β”‚ (W:98 C:72 T:65 R:54) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Experts Consulted β”‚ 10 unique β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Tensions Resolved β”‚ 6/6 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Final Velocity β”‚ 0 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +Converged Decisions +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Topic β”‚ Decision β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Architecture β”‚ Storage abstraction with provider trait β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Key Hierarchy β”‚ UMKβ†’KEKβ†’DEK, identical code path everywhere β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Local Dev β”‚ Docker required, DynamoDB Local mandatory β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Testing β”‚ NIST KAT + reference vectors + property tests β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +Resolved Tensions +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ID β”‚ Resolution β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ T0001 β”‚ Local keys never rotated - disposable with DB reset β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ T0002 β”‚ Audit logs include trace_id with hash chain β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +All experts signaled [MOVE:CONVERGE]. Velocity = 0. +``` + +This summary goes in the final `verdict.md` and is displayed to the user. + +### 8. SKILL.md Updates + +**Update** Key Rules: + +```markdown +8. **TRACK VELOCITY CORRECTLY** β€” Velocity = open_tensions + new_perspectives. This is "work remaining," not score delta. Convergence requires velocity = 0. + +9. **REQUIRE UNANIMOUS CONVERGENCE** β€” All experts must signal `[MOVE:CONVERGE]` before declaring convergence. 100%, not majority. + +10. **BOTH CONDITIONS REQUIRED** β€” Convergence requires BOTH velocity = 0 AND 100% expert convergence. Either condition failing means another round. + +11. **MAX ROUNDS = 10** β€” Safety valve. If round 10 completes without convergence, force it with a warning in the verdict. + +12. **OUTPUT CONVERGENCE SUMMARY** β€” When convergence achieved, output the formatted summary table showing: rounds, total ALIGNMENT (with W/C/T/R breakdown), experts consulted, tensions resolved, converged decisions, and resolved tensions. +``` + +**Add** new section "Driving Velocity to Zero": + +```markdown +## Driving Velocity to Zero + +When velocity > 0 after a round, the Judge must drive it down: + +### If Open Tensions > 0: +1. **Audit**: List tensions without `[RE:RESOLVE]` or `[ACCEPTED UNRESOLVED]` +2. **Assign Ownership**: Each tension needs an expert responsible for resolution +3. **Evolve Panel if Needed**: Pull from pool or create targeted expert +4. **Spawn Round**: Prompts emphasize tension resolution + +### If New Perspectives > 0: +1. **Assess**: Are these perspectives being integrated or creating new tensions? +2. **If creating tensions**: Address tensions first +3. **If integrating smoothly**: Continue β€” perspectives will stop emerging as coverage completes + +### If Converge % < 100%: +1. **Identify Holdouts**: Which experts haven't signaled `[MOVE:CONVERGE]`? +2. **Understand Why**: Are they defending open tensions? Surfacing new perspectives? +3. **Address Root Cause**: Usually ties back to velocity > 0 + +**Example:** + +Round 2 Summary: +- Velocity: 3 (1 tension + 2 perspectives) +- Converge %: 50% + +Analysis: +- T0001 still open β€” Muffin defending income mandate +- P0201, P0202 are new this round β€” still being integrated +- Donut, Eclair, Brioche haven't converged β€” waiting on T0001 resolution + +Action: Creating "Income Strategist" to address T0001. Expecting velocity β†’ 0 next round. +``` + +### 9. Dialogue Storage Location + +**Current:** Dialogues stored in `/tmp/blue-dialogue//` + +**Problem:** +- Invisible to users browsing the repo +- Lost on reboot +- Not tracked in git +- Hard to find and reference later + +**Proposed:** Store in `.blue/dialogues//` + +``` +.blue/ +β”œβ”€β”€ docs/ +β”‚ β”œβ”€β”€ adrs/ +β”‚ └── rfcs/ +└── dialogues/ ← NEW + └── 2026-02-03T1423Z-nvidia-investment/ + β”œβ”€β”€ dialogue.md + β”œβ”€β”€ expert-pool.json + β”œβ”€β”€ round-0/ + β”‚ β”œβ”€β”€ panel.json + β”‚ β”œβ”€β”€ muffin.md + β”‚ β”œβ”€β”€ cupcake.md + β”‚ └── round-0.summary.md + β”œβ”€β”€ round-1/ + β”‚ └── ... + β”œβ”€β”€ scoreboard.md + └── verdict.md +``` + +**Benefits:** +- Visible in repo structure +- Can be git-tracked (optional) +- Easy to reference: "see `.blue/dialogues/2026-02-03T1423Z-nvidia-investment/`" +- Survives reboots +- Natural home alongside other Blue artifacts +- ISO prefix sorts chronologically + +**Naming convention:** `-/` + +Format: `YYYY-MM-DDTHHMMZ-` (same as spikes) + +Examples: +- `2026-02-03T1423Z-nvidia-investment/` +- `2026-02-03T0900Z-rfc-0057-convergence-discipline/` + +### 10. Database & MCP Integration + +Extend the RFC 0051 schema to track velocity and convergence with safeguards. + +#### Schema Extension + +```sql +-- ================================================================ +-- ROUNDS (extends existing - add ALIGNMENT components & convergence) +-- ================================================================ +ALTER TABLE rounds ADD COLUMN score_wisdom INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN score_consistency INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN score_truth INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN score_relationships INTEGER NOT NULL DEFAULT 0; +-- score already exists as total; can be computed or stored + +ALTER TABLE rounds ADD COLUMN open_tensions INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN new_perspectives INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN velocity INTEGER GENERATED ALWAYS AS (open_tensions + new_perspectives) STORED; +ALTER TABLE rounds ADD COLUMN converge_signals INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN panel_size INTEGER NOT NULL DEFAULT 0; +ALTER TABLE rounds ADD COLUMN converge_percent REAL GENERATED ALWAYS AS ( + CASE WHEN panel_size > 0 THEN (converge_signals * 100.0 / panel_size) ELSE 0 END +) STORED; + +-- Convergence gate constraint: verdict requires velocity=0 AND converge_percent=100 +-- (enforced at MCP layer, not DB, for better error messages) + +-- ================================================================ +-- CONVERGENCE_SIGNALS (track per-expert convergence signals) +-- ================================================================ +CREATE TABLE convergence_signals ( + dialogue_id TEXT NOT NULL, + round INTEGER NOT NULL, + expert_name TEXT NOT NULL, + signaled_at TEXT NOT NULL, -- ISO 8601 timestamp + + PRIMARY KEY (dialogue_id, round, expert_name), + FOREIGN KEY (dialogue_id) REFERENCES dialogues(dialogue_id) +); + +CREATE INDEX idx_convergence_by_round ON convergence_signals(dialogue_id, round); + +-- ================================================================ +-- SCOREBOARD (denormalized view for efficient queries) +-- ================================================================ +CREATE VIEW scoreboard AS +SELECT + r.dialogue_id, + r.round, + r.score_wisdom AS W, + r.score_consistency AS C, + r.score_truth AS T, + r.score_relationships AS R, + r.score AS total, + r.open_tensions, + r.new_perspectives, + r.velocity, + r.converge_signals, + r.panel_size, + r.converge_percent, + (SELECT SUM(score) FROM rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_score, + (SELECT SUM(score_wisdom) FROM rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_W, + (SELECT SUM(score_consistency) FROM rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_C, + (SELECT SUM(score_truth) FROM rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_T, + (SELECT SUM(score_relationships) FROM rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_R +FROM rounds r +ORDER BY r.dialogue_id, r.round; +``` + +#### Export Format Extension + +The `blue_dialogue_export` output includes the full scoreboard: + +```json +{ + "dialogue_id": "nvidia-investment", + "title": "NVIDIA Investment Decision", + "scoreboard": { + "rounds": [ + { + "round": 0, + "score": { "W": 45, "C": 30, "T": 25, "R": 25, "total": 125 }, + "velocity": { "open_tensions": 3, "new_perspectives": 8, "total": 11 }, + "convergence": { "signals": 0, "panel_size": 6, "percent": 0 } + }, + { + "round": 1, + "score": { "W": 32, "C": 22, "T": 18, "R": 17, "total": 89 }, + "velocity": { "open_tensions": 1, "new_perspectives": 2, "total": 3 }, + "convergence": { "signals": 3, "panel_size": 6, "percent": 50 } + }, + { + "round": 2, + "score": { "W": 18, "C": 12, "T": 8, "R": 7, "total": 45 }, + "velocity": { "open_tensions": 0, "new_perspectives": 0, "total": 0 }, + "convergence": { "signals": 6, "panel_size": 6, "percent": 100 } + } + ], + "totals": { + "rounds": 3, + "alignment": { "W": 95, "C": 64, "T": 51, "R": 49, "total": 259 }, + "experts_consulted": 10, + "tensions_resolved": 6, + "final_velocity": 0, + "convergence_achieved": true, + "convergence_reason": "velocity=0, unanimous" + } + }, + "convergence_signals": [ + { "round": 1, "expert": "muffin", "signaled_at": "2026-02-03T15:23:00Z" }, + { "round": 1, "expert": "cupcake", "signaled_at": "2026-02-03T15:24:00Z" }, + { "round": 1, "expert": "scone", "signaled_at": "2026-02-03T15:25:00Z" }, + { "round": 2, "expert": "donut", "signaled_at": "2026-02-03T16:10:00Z" }, + { "round": 2, "expert": "eclair", "signaled_at": "2026-02-03T16:11:00Z" }, + { "round": 2, "expert": "brioche", "signaled_at": "2026-02-03T16:12:00Z" } + ], + "experts": [...], + "rounds": [...], + "perspectives": [...], + "tensions": [...], + "verdicts": [...] +} +``` + +#### Query Examples + +```sql +-- Get scoreboard for a dialogue +SELECT * FROM scoreboard WHERE dialogue_id = 'nvidia-investment'; + +-- Check if dialogue can converge +SELECT + dialogue_id, + round, + velocity, + converge_percent, + CASE + WHEN velocity = 0 AND converge_percent = 100 THEN 'CAN_CONVERGE' + WHEN velocity > 0 THEN 'BLOCKED: velocity=' || velocity + ELSE 'BLOCKED: converge=' || converge_percent || '%' + END AS status +FROM scoreboard +WHERE dialogue_id = 'nvidia-investment' +ORDER BY round DESC +LIMIT 1; + +-- Get cumulative ALIGNMENT breakdown +SELECT + dialogue_id, + MAX(cumulative_score) AS total_alignment, + MAX(cumulative_W) AS wisdom, + MAX(cumulative_C) AS consistency, + MAX(cumulative_T) AS truth, + MAX(cumulative_R) AS relationships +FROM scoreboard +WHERE dialogue_id = 'nvidia-investment'; +``` + +#### MCP Validation Layer + +**New validation rules (enforced before DB operations):** + +| Error Code | Trigger | Message Template | +|------------|---------|------------------| +| `velocity_not_zero` | Verdict with velocity > 0 | `Cannot register verdict: velocity={velocity} (open_tensions={ot}, new_perspectives={np}). Resolve tensions and integrate perspectives first.` | +| `convergence_not_unanimous` | Verdict with converge < 100% | `Cannot register verdict: convergence={pct}% ({signals}/{panel}). All experts must signal [MOVE:CONVERGE].` | +| `forced_convergence_no_warning` | Max rounds verdict without warning | `Forced convergence at max rounds requires explicit warning in verdict description.` | + +**Validation order for `blue_dialogue_verdict_register`:** + +1. **Round exists** β€” Check round data is registered +2. **Velocity gate** β€” Check `velocity == 0` (or max rounds reached) +3. **Convergence gate** β€” Check `converge_percent == 100` (or max rounds reached) +4. **Forced convergence warning** β€” If max rounds, require warning text + +**Error response format (per RFC 0051 pattern):** + +```json +{ + "status": "error", + "error_code": "velocity_not_zero", + "message": "Cannot register verdict: velocity=3 (open_tensions=1, new_perspectives=2)", + "field": "velocity", + "value": 3, + "constraint": "convergence_gate", + "suggestion": "Resolve T0001 and integrate P0201, P0202 before declaring convergence", + "context": { + "open_tensions": ["T0001"], + "new_perspectives": ["P0201", "P0202"], + "converge_percent": 50, + "missing_signals": ["Donut", "Eclair", "Brioche"] + } +} +``` + +#### MCP Tool Updates + +**`blue_dialogue_round_register`** β€” Add required fields: + +```json +{ + "dialogue_id": "nvidia-investment", + "round": 2, + "score": 45, + "score_components": { "W": 18, "C": 12, "T": 8, "R": 7 }, + "open_tensions": 0, + "new_perspectives": 0, + "converge_signals": ["Muffin", "Cupcake", "Scone", "Donut", "Eclair", "Brioche"], + "panel": ["Muffin", "Cupcake", "Scone", "Donut", "Eclair", "Brioche"], + "perspectives": [...], + "tensions": [...], + "expert_scores": {...} +} +``` + +**`blue_dialogue_round_context`** β€” Return velocity/convergence in response: + +```json +{ + "round": 2, + "velocity": { + "open_tensions": 0, + "new_perspectives": 0, + "total": 0 + }, + "convergence": { + "signals": 6, + "panel_size": 6, + "percent": 100, + "missing": [] + }, + "can_converge": true, + "convergence_blockers": [] +} +``` + +**`blue_dialogue_verdict_register`** β€” Gated by validation: + +```json +{ + "dialogue_id": "nvidia-investment", + "verdict_id": "final", + "verdict_type": "final", + "round": 2, + "recommendation": "APPROVE with options overlay", + "forced": false // true only if max rounds reached +} +``` + +If `forced: true`, requires `warning` field explaining why convergence was forced. + +#### Atomic Transactions + +Per RFC 0051 pattern: +- **All-or-nothing**: If validation fails, entire operation rejected +- **Detailed errors**: Response includes all validation failures +- **Judge recovery**: Structured errors allow programmatic correction + +### 11. ADR 0014 Update + +Update ADR 0014 convergence criteria: + +**Current:** +```markdown +1. **Plateau**: Velocity β‰ˆ 0 for two consecutive rounds +2. **Full Coverage**: All perspectives integrated +3. **Zero Tensions**: All `[TENSION]` markers have `[RESOLVED]` +4. **Mutual Recognition**: Majority signal `[CONVERGENCE CONFIRMED]` +5. **Max Rounds**: Safety valve reached +``` + +**Proposed:** +```markdown +Convergence requires ALL of: +1. **Velocity = 0**: open_tensions + new_perspectives == 0 +2. **Unanimous Recognition**: 100% of experts signal `[MOVE:CONVERGE]` + +Safety valve: +3. **Max Rounds**: 10 rounds (forces convergence with warning) +``` + +Key changes: +- Velocity redefined as "work remaining" (open tensions + new perspectives) +- Unanimous expert agreement required (not majority) +- Max rounds increased to 10 +- Both conditions must be true (AND, not OR) + +## Implementation + +### Phase 1: SKILL.md Update βœ… +- [x] Update velocity definition: `open_tensions + new_perspectives` +- [x] Add unanimous convergence requirement +- [x] Update max rounds to 10 +- [x] Update scoreboard format example (W/C/T/R columns) +- [x] Update default output_dir to `.blue/dialogues/` +- [x] Add "Driving Velocity to Zero" section +- [x] Add convergence summary template + +### Phase 2: ADR 0014 Update βœ… +- [x] Redefine velocity as "work remaining" +- [x] Change from OR to AND logic for convergence +- [x] Change from majority to unanimous expert agreement +- [x] Update max rounds to 10 + +### Phase 3: Database Schema Update βœ… +- [x] Add columns to `rounds` table: `score_wisdom`, `score_consistency`, `score_truth`, `score_relationships` +- [x] Add columns to `rounds` table: `open_tensions`, `new_perspectives`, `converge_signals`, `panel_size` +- [x] Add computed columns: `velocity`, `converge_percent` +- [x] Create `convergence_signals` table for per-expert tracking +- [x] Create `scoreboard` view for efficient queries +- [x] Add indices for query performance +- [x] Add RFC 0057 tests (42 tests pass) + +### Phase 4: MCP Validation Layer βœ… +- [x] Add `velocity_not_zero` validation rule +- [x] Add `convergence_not_unanimous` validation rule +- [x] Add `forced_convergence_no_warning` validation rule +- [x] Return structured errors with context (open tensions, missing signals) +- [x] Atomic transactions: all-or-nothing with detailed errors + +### Phase 5: MCP Tool Updates βœ… +- [x] `blue_dialogue_round_register`: Add `open_tensions`, `new_perspectives`, `converge_signals`, `panel_size`, `score_components` +- [x] `blue_dialogue_round_context`: Return velocity/convergence status and blockers +- [x] `blue_dialogue_verdict_register`: Gate on velocity=0 AND converge=100% (or forced with warning) +- [x] `blue_dialogue_create`: Default `output_dir` to `.blue/dialogues/-` +- [x] `blue_dialogue_export`: Include full scoreboard with cumulative totals + +### Phase 6: Judge Protocol Update βœ… +- [x] Replace `velocity_threshold` with velocity calculation instructions +- [x] Add `converge_percent` tracking requirement +- [x] Update `max_rounds` default to 10 +- [x] Document convergence gate validation +- [x] Add scoreboard format with RFC 0057 columns + +### Phase 7: Create .blue/dialogues/ Directory βœ… +- [x] Add `.blue/dialogues/.gitkeep` or initial README +- [ ] Update `.gitignore` if dialogues should not be tracked (optional) + +### Phase 8: CLI Parity (High Priority) +- [ ] Add `blue dialogue` subcommand with all dialogue tools +- [ ] Add `blue adr` subcommand with all ADR tools +- [ ] Add `blue spike` subcommand with spike tools +- [ ] CLI calls same handler functions as MCP tools (single implementation) +- [ ] Add `--help` documentation for all new commands + +### Phase 9: CLI Parity (Medium Priority) +- [ ] Add `blue audit` subcommand +- [ ] Add `blue prd` subcommand +- [ ] Add `blue reminder` subcommand + +### Phase 10: CLI Parity (Low Priority) +- [ ] Add `blue staging` subcommand (if not already complete) +- [ ] Add `blue llm` subcommand +- [ ] Add `blue postmortem` subcommand +- [ ] Add `blue runbook` subcommand + +## Success Criteria + +1. Velocity = open_tensions + new_perspectives (not score delta) +2. Convergence requires velocity = 0 AND 100% expert convergence +3. Max rounds = 10 (safety valve) +4. Scoreboard shows: W, C, T, R, Score, Open Tensions, New Perspectives, Velocity, Converge % +5. Judge never declares convergence unless both conditions met (except max rounds) +6. Forced convergence at max rounds includes warning in verdict +7. Dialogues stored in `.blue/dialogues/-/` by default +8. MCP blocks verdict registration when velocity > 0 (structured error with context) +9. MCP blocks verdict registration when converge < 100% (lists missing signals) +10. All validation errors return actionable suggestions per RFC 0051 pattern +11. Database stores W/C/T/R components per round (not just total score) +12. `scoreboard` view provides efficient query access to all convergence metrics +13. `blue_dialogue_export` includes full scoreboard with cumulative totals +14. Convergence signals tracked per-expert with timestamps for audit trail + +### 12. CLI Parity for MCP Commands + +**Bug:** The Judge tried to run `blue dialogue create` as a CLI command, but it doesn't exist. Many MCP tools lack CLI equivalents, causing silent failures when invoked from bash. + +**Problem:** +```bash +# These don't exist as CLI commands: +blue dialogue create --title "..." +blue adr create --title "..." +blue spike create --title "..." + +# Only MCP tools work: +blue_dialogue_create(title="...") +``` + +**Proposed:** Add CLI subcommands that wrap MCP functionality for all tool groups. + +#### Dialogue Commands (Priority: High) + +```bash +blue dialogue create --title "..." --question "..." --output-dir ".blue/dialogues/..." +blue dialogue list +blue dialogue get --id "..." +blue dialogue round-context --id "..." --round 1 +blue dialogue round-register --id "..." --round 1 --data round-1.json +blue dialogue evolve-panel --id "..." --round 1 --panel panel.json +blue dialogue verdict --id "..." --round 2 +blue dialogue export --id "..." +``` + +| CLI Command | MCP Tool | +|-------------|----------| +| `blue dialogue create` | `blue_dialogue_create` | +| `blue dialogue list` | `blue_dialogue_list` | +| `blue dialogue get` | `blue_dialogue_get` | +| `blue dialogue round-context` | `blue_dialogue_round_context` | +| `blue dialogue round-register` | `blue_dialogue_round_register` | +| `blue dialogue evolve-panel` | `blue_dialogue_evolve_panel` | +| `blue dialogue verdict` | `blue_dialogue_verdict_register` | +| `blue dialogue export` | `blue_dialogue_export` | + +#### ADR Commands (Priority: High) + +```bash +blue adr create --title "..." --status accepted +blue adr list +blue adr get --number 0014 +blue adr relevant --query "alignment scoring" +blue adr audit +``` + +| CLI Command | MCP Tool | +|-------------|----------| +| `blue adr create` | `blue_adr_create` | +| `blue adr list` | `blue_adr_list` | +| `blue adr get` | `blue_adr_get` | +| `blue adr relevant` | `blue_adr_relevant` | +| `blue adr audit` | `blue_adr_audit` | + +#### Spike Commands (Priority: Medium) + +```bash +blue spike create --title "..." --rfc 0057 +blue spike complete --path ".blue/docs/spikes/..." +``` + +| CLI Command | MCP Tool | +|-------------|----------| +| `blue spike create` | `blue_spike_create` | +| `blue spike complete` | `blue_spike_complete` | + +#### Audit Commands (Priority: Medium) + +```bash +blue audit create --title "..." --scope "security" +blue audit list +blue audit get --id "..." +blue audit complete --id "..." +``` + +#### Other Commands (Priority: Low) + +| Group | CLI Commands | MCP Tools | +|-------|--------------|-----------| +| `blue prd` | create, get, approve, complete, list | `blue_prd_*` | +| `blue reminder` | create, list, snooze, clear | `blue_reminder_*` | +| `blue staging` | create, destroy, lock, unlock, status, cost | `blue_staging_*` | +| `blue llm` | start, stop, status, providers | `blue_llm_*` | +| `blue postmortem` | create, action-to-rfc | `blue_postmortem_*` | +| `blue runbook` | create, update, lookup, actions | `blue_runbook_*` | + +#### Implementation Pattern + +All CLI commands wrap MCP handlers (single implementation): + +```rust +// In blue-cli/src/main.rs +Commands::Dialogue { command } => match command { + DialogueCommands::Create { title, question, output_dir } => { + // Call same handler as MCP + let result = dialogue::handle_dialogue_create(&json!({ + "title": title, + "question": question, + "output_dir": output_dir, + })).await?; + println!("{}", serde_json::to_string_pretty(&result)?); + } +} +``` + +This ensures CLI and MCP always behave identically. + +## Risks + +| Risk | Mitigation | +|------|------------| +| Infinite loops (tensions create more tensions) | Max rounds safety valve still applies | +| Over-aggressive tension hunting | Judge discretion on materiality | +| Slower convergence | Correct convergence > fast convergence | +| CLI/MCP parity drift | Single implementation, CLI wraps MCP handlers | + +## Open Questions + +1. Should MCP tools hard-block verdict registration with open tensions? +2. Should there be a "tension materiality" threshold (ignore minor tensions)? +3. Should ACCEPTED UNRESOLVED require unanimous expert acknowledgment? + +--- + +*"The elephant cannot be described while the blind men are still arguing about the trunk."* diff --git a/crates/blue-core/src/alignment_db.rs b/crates/blue-core/src/alignment_db.rs index ffab515..e3f87e4 100644 --- a/crates/blue-core/src/alignment_db.rs +++ b/crates/blue-core/src/alignment_db.rs @@ -1048,21 +1048,93 @@ pub fn get_experts( Ok(experts) } -/// Create a new round +/// RFC 0057: Score components for ALIGNMENT breakdown +#[derive(Debug, Clone, Default)] +pub struct ScoreComponents { + pub wisdom: i32, + pub consistency: i32, + pub truth: i32, + pub relationships: i32, +} + +impl ScoreComponents { + pub fn total(&self) -> i32 { + self.wisdom + self.consistency + self.truth + self.relationships + } +} + +/// RFC 0057: Convergence metrics for a round +#[derive(Debug, Clone, Default)] +pub struct ConvergenceMetrics { + pub open_tensions: i32, + pub new_perspectives: i32, + pub converge_signals: i32, + pub panel_size: i32, +} + +impl ConvergenceMetrics { + pub fn velocity(&self) -> i32 { + self.open_tensions + self.new_perspectives + } + + pub fn converge_percent(&self) -> f64 { + if self.panel_size > 0 { + (self.converge_signals as f64 * 100.0) / self.panel_size as f64 + } else { + 0.0 + } + } + + pub fn can_converge(&self) -> bool { + self.velocity() == 0 && self.converge_percent() >= 100.0 + } +} + +/// Create a new round (legacy signature for backward compatibility) pub fn create_round( conn: &Connection, dialogue_id: &str, round: i32, title: Option<&str>, score: i32, +) -> Result<(), AlignmentDbError> { + create_round_with_metrics( + conn, + dialogue_id, + round, + title, + score, + None, + None, + ) +} + +/// RFC 0057: Create a new round with full metrics +pub fn create_round_with_metrics( + conn: &Connection, + dialogue_id: &str, + round: i32, + title: Option<&str>, + score: i32, + score_components: Option<&ScoreComponents>, + convergence: Option<&ConvergenceMetrics>, ) -> Result<(), AlignmentDbError> { let now = Utc::now().to_rfc3339(); + let sc = score_components.cloned().unwrap_or_default(); + let cm = convergence.cloned().unwrap_or_default(); + conn.execute( "INSERT INTO alignment_rounds - (dialogue_id, round, title, score, status, created_at) - VALUES (?1, ?2, ?3, ?4, ?5, ?6)", - params![dialogue_id, round, title, score, "open", now], + (dialogue_id, round, title, score, score_wisdom, score_consistency, score_truth, score_relationships, + open_tensions, new_perspectives, converge_signals, panel_size, status, created_at) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)", + params![ + dialogue_id, round, title, score, + sc.wisdom, sc.consistency, sc.truth, sc.relationships, + cm.open_tensions, cm.new_perspectives, cm.converge_signals, cm.panel_size, + "open", now + ], )?; // Update dialogue total rounds @@ -1075,6 +1147,134 @@ pub fn create_round( Ok(()) } +/// RFC 0057: Record a convergence signal from an expert +pub fn record_convergence_signal( + conn: &Connection, + dialogue_id: &str, + round: i32, + expert_name: &str, +) -> Result<(), AlignmentDbError> { + let now = Utc::now().to_rfc3339(); + + conn.execute( + "INSERT OR REPLACE INTO alignment_convergence_signals + (dialogue_id, round, expert_name, signaled_at) + VALUES (?1, ?2, ?3, ?4)", + params![dialogue_id, round, expert_name, now], + )?; + + Ok(()) +} + +/// RFC 0057: Get convergence signals for a round +pub fn get_convergence_signals( + conn: &Connection, + dialogue_id: &str, + round: i32, +) -> Result, AlignmentDbError> { + let mut stmt = conn.prepare( + "SELECT expert_name FROM alignment_convergence_signals + WHERE dialogue_id = ?1 AND round = ?2" + )?; + + let signals = stmt + .query_map(params![dialogue_id, round], |row| row.get(0))? + .collect::, _>>()?; + + Ok(signals) +} + +/// RFC 0057: Get scoreboard data for a dialogue +pub fn get_scoreboard( + conn: &Connection, + dialogue_id: &str, +) -> Result, AlignmentDbError> { + let mut stmt = conn.prepare( + "SELECT round, W, C, T, R, total, open_tensions, new_perspectives, velocity, + converge_signals, panel_size, converge_percent, + cumulative_score, cumulative_W, cumulative_C, cumulative_T, cumulative_R + FROM alignment_scoreboard WHERE dialogue_id = ?1 ORDER BY round" + )?; + + let rows = stmt + .query_map(params![dialogue_id], |row| { + Ok(ScoreboardRow { + round: row.get(0)?, + w: row.get(1)?, + c: row.get(2)?, + t: row.get(3)?, + r: row.get(4)?, + total: row.get(5)?, + open_tensions: row.get(6)?, + new_perspectives: row.get(7)?, + velocity: row.get(8)?, + converge_signals: row.get(9)?, + panel_size: row.get(10)?, + converge_percent: row.get(11)?, + cumulative_score: row.get(12)?, + cumulative_w: row.get(13)?, + cumulative_c: row.get(14)?, + cumulative_t: row.get(15)?, + cumulative_r: row.get(16)?, + }) + })? + .collect::, _>>()?; + + Ok(rows) +} + +/// RFC 0057: Scoreboard row data +#[derive(Debug, Clone)] +pub struct ScoreboardRow { + pub round: i32, + pub w: i32, + pub c: i32, + pub t: i32, + pub r: i32, + pub total: i32, + pub open_tensions: i32, + pub new_perspectives: i32, + pub velocity: i32, + pub converge_signals: i32, + pub panel_size: i32, + pub converge_percent: f64, + pub cumulative_score: i32, + pub cumulative_w: i32, + pub cumulative_c: i32, + pub cumulative_t: i32, + pub cumulative_r: i32, +} + +/// RFC 0057: Check if dialogue can converge +pub fn can_dialogue_converge( + conn: &Connection, + dialogue_id: &str, +) -> Result<(bool, Vec), AlignmentDbError> { + let scoreboard = get_scoreboard(conn, dialogue_id)?; + + if let Some(last_round) = scoreboard.last() { + let mut blockers = Vec::new(); + + if last_round.velocity > 0 { + blockers.push(format!( + "velocity={} (open_tensions={}, new_perspectives={})", + last_round.velocity, last_round.open_tensions, last_round.new_perspectives + )); + } + + if last_round.converge_percent < 100.0 { + blockers.push(format!( + "converge_percent={:.0}% ({}/{})", + last_round.converge_percent, last_round.converge_signals, last_round.panel_size + )); + } + + Ok((blockers.is_empty(), blockers)) + } else { + Ok((false, vec!["no rounds registered".to_string()])) + } +} + /// Get next sequence number for an entity type in a round pub fn next_seq( conn: &Connection, @@ -2226,6 +2426,16 @@ mod tests { round INTEGER NOT NULL, title TEXT, score INTEGER NOT NULL, + -- RFC 0057: ALIGNMENT score components + score_wisdom INTEGER NOT NULL DEFAULT 0, + score_consistency INTEGER NOT NULL DEFAULT 0, + score_truth INTEGER NOT NULL DEFAULT 0, + score_relationships INTEGER NOT NULL DEFAULT 0, + -- RFC 0057: Velocity & convergence tracking + open_tensions INTEGER NOT NULL DEFAULT 0, + new_perspectives INTEGER NOT NULL DEFAULT 0, + converge_signals INTEGER NOT NULL DEFAULT 0, + panel_size INTEGER NOT NULL DEFAULT 0, summary TEXT, status TEXT NOT NULL DEFAULT 'open', created_at TEXT NOT NULL, @@ -2233,6 +2443,15 @@ mod tests { PRIMARY KEY (dialogue_id, round) ); + -- RFC 0057: Track per-expert convergence signals + CREATE TABLE alignment_convergence_signals ( + dialogue_id TEXT NOT NULL, + round INTEGER NOT NULL, + expert_name TEXT NOT NULL, + signaled_at TEXT NOT NULL, + PRIMARY KEY (dialogue_id, round, expert_name) + ); + CREATE TABLE alignment_perspectives ( dialogue_id TEXT NOT NULL, round INTEGER NOT NULL, @@ -2382,6 +2601,31 @@ mod tests { CREATE INDEX idx_refs_dialogue ON alignment_refs(dialogue_id); CREATE INDEX idx_refs_target ON alignment_refs(dialogue_id, target_id); CREATE INDEX idx_verdicts_dialogue ON alignment_verdicts(dialogue_id); + CREATE INDEX idx_convergence_signals_round ON alignment_convergence_signals(dialogue_id, round); + + -- RFC 0057: Scoreboard view for efficient convergence queries + CREATE VIEW alignment_scoreboard AS + SELECT + r.dialogue_id, + r.round, + r.score_wisdom AS W, + r.score_consistency AS C, + r.score_truth AS T, + r.score_relationships AS R, + r.score AS total, + r.open_tensions, + r.new_perspectives, + (r.open_tensions + r.new_perspectives) AS velocity, + r.converge_signals, + r.panel_size, + CASE WHEN r.panel_size > 0 THEN (r.converge_signals * 100.0 / r.panel_size) ELSE 0 END AS converge_percent, + (SELECT SUM(score) FROM alignment_rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_score, + (SELECT SUM(score_wisdom) FROM alignment_rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_W, + (SELECT SUM(score_consistency) FROM alignment_rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_C, + (SELECT SUM(score_truth) FROM alignment_rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_T, + (SELECT SUM(score_relationships) FROM alignment_rounds r2 WHERE r2.dialogue_id = r.dialogue_id AND r2.round <= r.round) AS cumulative_R + FROM alignment_rounds r + ORDER BY r.dialogue_id, r.round; "#, ) .unwrap(); @@ -3661,4 +3905,226 @@ mod tests { assert_eq!(ref_row.0, p1); assert_eq!(ref_row.1, t1); } + + // ==================== RFC 0057 Tests ==================== + + #[test] + fn test_score_components() { + let sc = ScoreComponents { + wisdom: 10, + consistency: 5, + truth: 8, + relationships: 3, + }; + assert_eq!(sc.total(), 26); + + let empty = ScoreComponents::default(); + assert_eq!(empty.total(), 0); + } + + #[test] + fn test_convergence_metrics() { + // Can converge: velocity=0, 100% signals + let cm = ConvergenceMetrics { + open_tensions: 0, + new_perspectives: 0, + converge_signals: 12, + panel_size: 12, + }; + assert_eq!(cm.velocity(), 0); + assert_eq!(cm.converge_percent(), 100.0); + assert!(cm.can_converge()); + + // Cannot converge: velocity > 0 + let cm2 = ConvergenceMetrics { + open_tensions: 2, + new_perspectives: 1, + converge_signals: 12, + panel_size: 12, + }; + assert_eq!(cm2.velocity(), 3); + assert!(!cm2.can_converge()); + + // Cannot converge: not 100% signals + let cm3 = ConvergenceMetrics { + open_tensions: 0, + new_perspectives: 0, + converge_signals: 10, + panel_size: 12, + }; + assert_eq!(cm3.velocity(), 0); + assert!(cm3.converge_percent() < 100.0); + assert!(!cm3.can_converge()); + + // Edge case: panel_size = 0 + let cm4 = ConvergenceMetrics { + open_tensions: 0, + new_perspectives: 0, + converge_signals: 0, + panel_size: 0, + }; + assert_eq!(cm4.converge_percent(), 0.0); + assert!(!cm4.can_converge()); + } + + #[test] + fn test_create_round_with_metrics() { + let conn = setup_test_db(); + let dialogue_id = create_dialogue(&conn, "Metrics Test", None, None, None).unwrap(); + + let sc = ScoreComponents { + wisdom: 5, + consistency: 3, + truth: 4, + relationships: 2, + }; + let cm = ConvergenceMetrics { + open_tensions: 3, + new_perspectives: 2, + converge_signals: 8, + panel_size: 12, + }; + + create_round_with_metrics( + &conn, + &dialogue_id, + 0, + Some("Round 0"), + 14, // 5+3+4+2 + Some(&sc), + Some(&cm), + ).unwrap(); + + // Verify the round was created with correct metrics + let row: (i32, i32, i32, i32, i32, i32, i32, i32) = conn.query_row( + "SELECT score_wisdom, score_consistency, score_truth, score_relationships, + open_tensions, new_perspectives, converge_signals, panel_size + FROM alignment_rounds WHERE dialogue_id = ?1 AND round = 0", + params![dialogue_id], + |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?, + row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?)) + ).unwrap(); + + assert_eq!(row.0, 5); // wisdom + assert_eq!(row.1, 3); // consistency + assert_eq!(row.2, 4); // truth + assert_eq!(row.3, 2); // relationships + assert_eq!(row.4, 3); // open_tensions + assert_eq!(row.5, 2); // new_perspectives + assert_eq!(row.6, 8); // converge_signals + assert_eq!(row.7, 12); // panel_size + } + + #[test] + fn test_convergence_signals() { + let conn = setup_test_db(); + let dialogue_id = create_dialogue(&conn, "Signals Test", None, None, None).unwrap(); + create_round(&conn, &dialogue_id, 0, None, 10).unwrap(); + + // Record signals from multiple experts + record_convergence_signal(&conn, &dialogue_id, 0, "muffin").unwrap(); + record_convergence_signal(&conn, &dialogue_id, 0, "cupcake").unwrap(); + record_convergence_signal(&conn, &dialogue_id, 0, "scone").unwrap(); + + // Retrieve signals + let signals = get_convergence_signals(&conn, &dialogue_id, 0).unwrap(); + assert_eq!(signals.len(), 3); + assert!(signals.contains(&"muffin".to_string())); + assert!(signals.contains(&"cupcake".to_string())); + assert!(signals.contains(&"scone".to_string())); + + // Re-recording same signal should replace (OR REPLACE) + record_convergence_signal(&conn, &dialogue_id, 0, "muffin").unwrap(); + let signals2 = get_convergence_signals(&conn, &dialogue_id, 0).unwrap(); + assert_eq!(signals2.len(), 3); // Still 3, not 4 + } + + #[test] + fn test_get_scoreboard() { + let conn = setup_test_db(); + let dialogue_id = create_dialogue(&conn, "Scoreboard Test", None, None, None).unwrap(); + + // Create round 0 + let sc0 = ScoreComponents { wisdom: 10, consistency: 5, truth: 8, relationships: 3 }; + let cm0 = ConvergenceMetrics { + open_tensions: 5, + new_perspectives: 3, + converge_signals: 4, + panel_size: 12, + }; + create_round_with_metrics(&conn, &dialogue_id, 0, Some("Round 0"), 26, Some(&sc0), Some(&cm0)).unwrap(); + + // Create round 1 + let sc1 = ScoreComponents { wisdom: 8, consistency: 6, truth: 4, relationships: 2 }; + let cm1 = ConvergenceMetrics { + open_tensions: 2, + new_perspectives: 1, + converge_signals: 10, + panel_size: 12, + }; + create_round_with_metrics(&conn, &dialogue_id, 1, Some("Round 1"), 20, Some(&sc1), Some(&cm1)).unwrap(); + + // Get scoreboard + let scoreboard = get_scoreboard(&conn, &dialogue_id).unwrap(); + assert_eq!(scoreboard.len(), 2); + + // Round 0 + let r0 = &scoreboard[0]; + assert_eq!(r0.round, 0); + assert_eq!(r0.w, 10); + assert_eq!(r0.c, 5); + assert_eq!(r0.t, 8); + assert_eq!(r0.r, 3); + assert_eq!(r0.total, 26); + assert_eq!(r0.velocity, 8); // 5 + 3 + assert_eq!(r0.cumulative_score, 26); + + // Round 1 + let r1 = &scoreboard[1]; + assert_eq!(r1.round, 1); + assert_eq!(r1.w, 8); + assert_eq!(r1.velocity, 3); // 2 + 1 + assert_eq!(r1.cumulative_score, 46); // 26 + 20 + assert_eq!(r1.cumulative_w, 18); // 10 + 8 + } + + #[test] + fn test_can_dialogue_converge() { + let conn = setup_test_db(); + let dialogue_id = create_dialogue(&conn, "Converge Test", None, None, None).unwrap(); + + // No rounds yet + let (can, blockers) = can_dialogue_converge(&conn, &dialogue_id).unwrap(); + assert!(!can); + assert!(blockers.iter().any(|b| b.contains("no rounds"))); + + // Round with velocity > 0 and incomplete signals + let sc = ScoreComponents { wisdom: 5, consistency: 3, truth: 4, relationships: 2 }; + let cm = ConvergenceMetrics { + open_tensions: 2, + new_perspectives: 1, + converge_signals: 8, + panel_size: 12, + }; + create_round_with_metrics(&conn, &dialogue_id, 0, None, 14, Some(&sc), Some(&cm)).unwrap(); + + let (can, blockers) = can_dialogue_converge(&conn, &dialogue_id).unwrap(); + assert!(!can); + assert!(blockers.iter().any(|b| b.contains("velocity=3"))); + assert!(blockers.iter().any(|b| b.contains("converge_percent="))); + + // Round where convergence is possible + let sc2 = ScoreComponents { wisdom: 2, consistency: 1, truth: 1, relationships: 1 }; + let cm2 = ConvergenceMetrics { + open_tensions: 0, + new_perspectives: 0, + converge_signals: 12, + panel_size: 12, + }; + create_round_with_metrics(&conn, &dialogue_id, 1, None, 5, Some(&sc2), Some(&cm2)).unwrap(); + + let (can, blockers) = can_dialogue_converge(&conn, &dialogue_id).unwrap(); + assert!(can); + assert!(blockers.is_empty()); + } } diff --git a/crates/blue-mcp/src/handlers/dialogue.rs b/crates/blue-mcp/src/handlers/dialogue.rs index 1f899a5..d883b29 100644 --- a/crates/blue-mcp/src/handlers/dialogue.rs +++ b/crates/blue-mcp/src/handlers/dialogue.rs @@ -15,11 +15,14 @@ use blue_core::alignment_db::{ ValidationError, ValidationCollector, validate_ref_semantics, validate_display_id, get_dialogue, register_expert, get_experts, - create_round, register_perspective, register_tension, + create_round_with_metrics, register_perspective, register_tension, register_recommendation, register_evidence, register_claim, register_ref, register_verdict, update_tension_status, update_expert_score, get_perspectives, get_tensions, get_recommendations, get_evidence, get_claims, get_verdicts, display_id, parse_display_id, + // RFC 0057: Convergence discipline + ScoreComponents, ConvergenceMetrics, + can_dialogue_converge, get_scoreboard, }; use rand::Rng; use serde::{Deserialize, Serialize}; @@ -557,8 +560,9 @@ pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result-/ + // Format: .blue/dialogues/2026-02-03T1423Z-topic-name/ + let output_dir = format!(".blue/dialogues/{}-{}", timestamp, slug); fs::create_dir_all(&output_dir).map_err(|e| { ServerError::CommandFailed(format!("Failed to create output dir {}: {}", output_dir, e)) })?; @@ -1340,9 +1344,37 @@ All {agent_count} results return when complete WITH STRUCTURED CONFIRMATIONS. - Updated Perspectives Inventory (one row per [PERSPECTIVE Pnn:] marker) - Updated Tensions Tracker (one row per [TENSION Tn:] marker) Full agent responses stay in {output_dir}/round-N/*.md (ADR 0005: reference, don't copy). -7. CONVERGE: If velocity approaches 0 OR all tensions resolved β†’ declare convergence - Otherwise, start next round (agents will read Step 5 artifacts via CONTEXT_INSTRUCTIONS). - Maximum 5 rounds (safety valve) +7. CONVERGENCE CHECK (RFC 0057) β€” BOTH CONDITIONS REQUIRED: + a. Calculate VELOCITY = open_tensions + new_perspectives + - Count open/reopened tensions (not resolved or accepted) + - Count new perspectives surfaced THIS round + b. Calculate CONVERGE % = (experts_signaling_converge / panel_size) Γ— 100 + - Expert signals convergence via `[MOVE:CONVERGE]` marker in their response + c. Convergence ONLY when: velocity == 0 AND converge_percent == 100% + d. If velocity > 0: Start next round to resolve tensions/perspectives + e. If converge < 100%: Start next round β€” experts not aligned + f. Maximum 10 rounds (safety valve) β€” force convergence with warning + +=== SCOREBOARD FORMAT (RFC 0057) === + +The scoreboard tracks ALIGNMENT progress AND convergence metrics. + +```markdown +| Round | W | C | T | R | Score | Ξ£ | Open T | New P | Velocity | Converge % | +|-------|---|---|---|---|-------|---|--------|-------|----------|------------| +| 0 | 5 | 3 | 4 | 2 | 14 |14 | 3 | 5 | 8 | 0% | +| 1 | 8 | 4 | 5 | 3 | 20 |34 | 1 | 2 | 3 | 50% | +| 2 | 4 | 2 | 2 | 1 | 9 |43 | 0 | 0 | 0 | 100% | +``` + +Columns: +- W/C/T/R: Wisdom, Consistency, Truth, Relationships (ALIGNMENT components) +- Score: This round's total (W+C+T+R) +- Ξ£: Cumulative total score +- Open T: Open tensions count (velocity component 1) +- New P: New perspectives this round (velocity component 2) +- Velocity: Open T + New P (must be 0 to converge) +- Converge %: Experts signaling `[MOVE:CONVERGE]` / panel size Γ— 100 === TOKEN BUDGET === @@ -1352,6 +1384,7 @@ Both well under 25K limit. Opus usage minimized. AGENTS: {agent_names} OUTPUT DIR: {output_dir} +MAX ROUNDS: 10 FORMAT RULES β€” MANDATORY: - ALWAYS prefix agent names with their emoji (🧁 Muffin) not bare name (Muffin) @@ -1359,8 +1392,9 @@ FORMAT RULES β€” MANDATORY: - Expert Panel table columns: Agent | Role | Tier | Relevance | Emoji - Round headers use emoji prefix (### 🧁 Muffin) - Scores start at 0 β€” only fill after reading agent returns +- Scoreboard MUST include all RFC 0057 columns (W/C/T/R, Velocity, Converge %) -NOTE: blue_dialogue_round_prompt handles round-specific context automatically: +NOTE: blue_dialogue_round_prompt handles round-specific context (CONTEXT_INSTRUCTIONS) automatically: - Round 0: No context instructions (agents have no memory of each other) - Round 1+: Automatically includes READ CONTEXT block with correct paths"##, agent_count = agents.len(), @@ -1438,10 +1472,14 @@ You are not limited to the initial pool. If the dialogue surfaces a perspective "sources": sources, "output_dir": output_dir, "rotation": format!("{:?}", rotation).to_lowercase(), + // RFC 0057: Updated convergence params "convergence": { - "max_rounds": 5, - "velocity_threshold": 0.1, + "max_rounds": 10, "tension_resolution_gate": true, + // Velocity is now: open_tensions + new_perspectives (not threshold-based) + "velocity_formula": "open_tensions + new_perspectives", + // Convergence requires: velocity == 0 AND converge_percent == 100% + "convergence_formula": "velocity == 0 AND converge_percent == 100%", }, }); @@ -2047,6 +2085,31 @@ pub fn handle_round_context(state: &ProjectState, args: &Value) -> Result Result Result 0 || new_perspectives > 0 || converge_signals > 0 || panel_size > 0 { + Some(ConvergenceMetrics { + open_tensions, + new_perspectives, + converge_signals, + panel_size, + }) + } else { + None + } + }; + // Phase 2c: Batch validation - collect ALL errors before registration let validation_errors = validate_round_register_inputs(args); if !validation_errors.is_empty() { @@ -2419,9 +2515,17 @@ pub fn handle_round_register(state: &ProjectState, args: &Value) -> Result Result = None; + if verdict_type == VerdictType::Final { + let (can_converge, blockers) = can_dialogue_converge(conn, dialogue_id) + .map_err(|e| ServerError::CommandFailed(format!("Failed to check convergence: {}", e)))?; + + if !can_converge { + if force { + // Allow with warning + forced_warning = Some(format!( + "FORCED CONVERGENCE: Convergence criteria not met. Blockers: {}", + blockers.join("; ") + )); + } else { + // Block registration with structured error + let scoreboard = get_scoreboard(conn, dialogue_id).ok(); + let last_round = scoreboard.as_ref().and_then(|s| s.last()); + + return Ok(json!({ + "status": "error", + "error_type": "convergence_blocked", + "message": format!("Cannot register final verdict: convergence criteria not met"), + "blockers": blockers, + "current_state": { + "velocity": last_round.map(|r| r.velocity).unwrap_or(-1), + "open_tensions": last_round.map(|r| r.open_tensions).unwrap_or(0), + "new_perspectives": last_round.map(|r| r.new_perspectives).unwrap_or(0), + "converge_percent": last_round.map(|r| r.converge_percent).unwrap_or(0.0), + "converge_signals": last_round.map(|r| r.converge_signals).unwrap_or(0), + "panel_size": last_round.map(|r| r.panel_size).unwrap_or(0), + }, + "suggestion": "Either resolve remaining tensions/perspectives and get 100% expert convergence, or use force=true to override (not recommended).", + "hint": "Use blue_dialogue_round_context to see detailed convergence status" + })); + } + } + } + let verdict = Verdict { dialogue_id: dialogue_id.to_string(), verdict_id: verdict_id.to_string(), @@ -2730,14 +2875,22 @@ pub fn handle_verdict_register(state: &ProjectState, args: &Value) -> Result Result Result>(), + // RFC 0057: Full scoreboard with velocity and convergence tracking + "scoreboard": scoreboard.iter().map(|r| json!({ + "round": r.round, + "score": { + "W": r.w, + "C": r.c, + "T": r.t, + "R": r.r, + "total": r.total, + }, + "velocity": r.velocity, + "open_tensions": r.open_tensions, + "new_perspectives": r.new_perspectives, + "converge_signals": r.converge_signals, + "panel_size": r.panel_size, + "converge_percent": r.converge_percent, + "cumulative": { + "score": r.cumulative_score, + "W": r.cumulative_w, + "C": r.cumulative_c, + "T": r.cumulative_t, + "R": r.cumulative_r, + }, + })).collect::>(), "exported_at": chrono::Utc::now().to_rfc3339(), }); @@ -3170,9 +3351,11 @@ mod tests { // Must have output_dir assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/system-design"); - // Must have convergence params - assert_eq!(protocol["convergence"]["max_rounds"], 5); + // Must have convergence params (RFC 0057) + assert_eq!(protocol["convergence"]["max_rounds"], 10); assert!(protocol["convergence"]["tension_resolution_gate"].as_bool().unwrap()); + assert!(protocol["convergence"]["velocity_formula"].as_str().is_some()); + assert!(protocol["convergence"]["convergence_formula"].as_str().is_some()); } #[test] diff --git a/skills/alignment-play/SKILL.md b/skills/alignment-play/SKILL.md index 54add22..4954d23 100644 --- a/skills/alignment-play/SKILL.md +++ b/skills/alignment-play/SKILL.md @@ -22,7 +22,7 @@ Orchestrate multi-expert alignment dialogues using the N+1 agent architecture fr |-----------|---------|-------------| | `--panel-size` | pool size or 12 | Number of experts per round | | `--rotation` | `graduated` | Rotation mode: **graduated** (default), none, wildcards, full | -| `--max-rounds` | `12` | Maximum rounds before stopping | +| `--max-rounds` | `10` | Maximum rounds before stopping (RFC 0057) | | `--rfc` | none | Link dialogue to an RFC | ## How It Works @@ -75,7 +75,7 @@ The suggested panel is just that β€” a suggestion. **Review it before Round 0:** ```json { - "output_dir": "/tmp/blue-dialogue/data-design", + "output_dir": ".blue/dialogues/data-design", "round": 0, "panel": [ { "name": "Muffin", "role": "API Architect", "source": "pool" }, @@ -105,7 +105,7 @@ Use `blue_dialogue_evolve_panel` to specify your panel: ```json { - "output_dir": "/tmp/blue-dialogue/investment-strategy", + "output_dir": ".blue/dialogues/investment-strategy", "round": 1, "panel": [ { "name": "Muffin", "role": "Value Analyst", "source": "retained" }, @@ -120,7 +120,7 @@ Then spawn the panel using `blue_dialogue_round_prompt` with the `expert_source` ``` blue_dialogue_round_prompt( - output_dir="/tmp/blue-dialogue/investment-strategy", + output_dir=".blue/dialogues/investment-strategy", agent_name="Palmier", agent_emoji="🧁", agent_role="Geopolitical Risk Analyst", @@ -354,6 +354,94 @@ See the `alignment-expert` skill (`/alignment-expert`) for full syntax reference 5. **Spawn ALL agents in ONE message** β€” No first-mover advantage 6. **Follow the Judge Protocol exactly** β€” It contains the round workflow, artifact writing steps, scoring rules, and convergence criteria 7. **Use `general-purpose` subagent_type** β€” NOT `alignment-expert`. The general-purpose agents have access to all tools including Write, which is required for file output +8. **TRACK VELOCITY CORRECTLY** β€” Velocity = open_tensions + new_perspectives. This is "work remaining," not score delta. Convergence requires velocity = 0. (RFC 0057) +9. **REQUIRE UNANIMOUS CONVERGENCE** β€” All experts must signal `[MOVE:CONVERGE]` before declaring convergence. 100%, not majority. (RFC 0057) +10. **BOTH CONDITIONS REQUIRED** β€” Convergence requires BOTH velocity = 0 AND 100% expert convergence. Either condition failing means another round. (RFC 0057) +11. **MAX ROUNDS = 10** β€” Safety valve. If round 10 completes without convergence, force it with a warning in the verdict. (RFC 0057) +12. **OUTPUT CONVERGENCE SUMMARY** β€” When convergence achieved, output the formatted summary table showing: rounds, total ALIGNMENT (with W/C/T/R breakdown), experts consulted, tensions resolved, converged decisions, and resolved tensions. (RFC 0057) + +## Driving Velocity to Zero (RFC 0057) + +When velocity > 0 after a round, the Judge must drive it down: + +### If Open Tensions > 0: +1. **Audit**: List tensions without `[RE:RESOLVE]` or `[ACCEPTED UNRESOLVED]` +2. **Assign Ownership**: Each tension needs an expert responsible for resolution +3. **Evolve Panel if Needed**: Pull from pool or create targeted expert +4. **Spawn Round**: Prompts emphasize tension resolution + +### If New Perspectives > 0: +1. **Assess**: Are these perspectives being integrated or creating new tensions? +2. **If creating tensions**: Address tensions first +3. **If integrating smoothly**: Continue β€” perspectives will stop emerging as coverage completes + +### If Converge % < 100%: +1. **Identify Holdouts**: Which experts haven't signaled `[MOVE:CONVERGE]`? +2. **Understand Why**: Are they defending open tensions? Surfacing new perspectives? +3. **Address Root Cause**: Usually ties back to velocity > 0 + +## Scoreboard Format (RFC 0057) + +Track both ALIGNMENT components (W/C/T/R) and convergence metrics: + +```markdown +| Round | W | C | T | R | Score | Open Tensions | New Perspectives | Velocity | Converge % | +|-------|---|---|---|---|-------|---------------|------------------|----------|------------| +| 0 | 45| 30| 25| 25| 125 | 3 | 8 | 11 | 0% | +| 1 | 32| 22| 18| 17| 89 | 1 | 2 | 3 | 50% | +| 2 | 18| 12| 8 | 7 | 45 | 0 | 0 | 0 | 100% | + +**Total ALIGNMENT:** 259 (W:95 C:64 T:51 R:49) +**Max Rounds:** 10 +**Convergence:** βœ“ (velocity=0, unanimous) +``` + +- **W** = Wisdom (perspectives integrated, synthesis quality) +- **C** = Consistency (follows patterns, internally coherent) +- **T** = Truth (grounded in reality, no contradictions) +- **R** = Relationships (connections to other artifacts, graph completeness) + +## Convergence Summary Template (RFC 0057) + +When convergence is achieved, output this summary: + +``` +100% CONVERGENCE ACHIEVED + +Final Dialogue Summary +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Metric β”‚ Value β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Rounds β”‚ 3 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Total ALIGNMENT β”‚ 289 β”‚ +β”‚ (W:98 C:72 T:65 R:54) β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Experts Consulted β”‚ 10 unique β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Tensions Resolved β”‚ 6/6 β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Final Velocity β”‚ 0 β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +Converged Decisions +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Topic β”‚ Decision β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Architecture β”‚ Storage abstraction with provider trait β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ Testing β”‚ NIST KAT + reference vectors + property tests β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +Resolved Tensions +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ ID β”‚ Resolution β”‚ +β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ +β”‚ T0001 β”‚ Local keys never rotated - disposable with DB reset β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + +All experts signaled [MOVE:CONVERGE]. Velocity = 0. +``` ## The Spirit of the Dialogue