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/<ISO>-<name>/ 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 <noreply@anthropic.com>
This commit is contained in:
parent
1447a2a6d2
commit
b8337c43a2
6 changed files with 1683 additions and 40 deletions
7
.blue/dialogues/.gitkeep
Normal file
7
.blue/dialogues/.gitkeep
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Blue Dialogues
|
||||||
|
|
||||||
|
This directory stores alignment dialogue artifacts.
|
||||||
|
|
||||||
|
Naming convention: `YYYY-MM-DDTHHMMZ-<topic>/`
|
||||||
|
|
||||||
|
Example: `2026-02-03T1423Z-nvidia-investment/`
|
||||||
|
|
@ -125,16 +125,22 @@ Unbounded scoring:
|
||||||
- Reflects reality: there's always more ALIGNMENT to achieve
|
- Reflects reality: there's always more ALIGNMENT to achieve
|
||||||
- Makes velocity meaningful: +2 vs +20 tells you something
|
- 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)
|
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
|
## 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 │
|
│ EACH ROUND: Spawn N agents IN PARALLEL │
|
||||||
│ LOOP until: │
|
│ LOOP until (RFC 0057): │
|
||||||
│ - ALIGNMENT Plateau (velocity ≈ 0) │
|
│ - Velocity = 0 (open_tensions + new_perspectives) │
|
||||||
│ - All tensions resolved │
|
│ - 100% experts signal [MOVE:CONVERGE] │
|
||||||
│ - 💙 declares convergence │
|
│ - Max rounds reached (10, safety valve) │
|
||||||
│ - Max rounds reached (safety valve) │
|
|
||||||
└─────────────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -406,15 +411,21 @@ The N-agent parallel architecture provides:
|
||||||
- No race conditions (all write to separate outputs, Judge merges)
|
- No race conditions (all write to separate outputs, Judge merges)
|
||||||
- Claude Code's Task tool supports parallel spawning natively
|
- 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)
|
1. **Velocity = 0** - `open_tensions + new_perspectives == 0` ("work remaining" is zero)
|
||||||
2. **Full Coverage** - Perspectives Inventory has no ✗ items (all integrated or consciously deferred)
|
2. **Unanimous Recognition** - 100% of 🧁s signal `[MOVE:CONVERGE]`
|
||||||
3. **Zero Tensions** - All `[TENSION]` markers have matching `[RESOLVED]`
|
|
||||||
4. **Mutual Recognition** - Majority of 🧁s state they believe ALIGNMENT has been reached
|
Plus safety valve:
|
||||||
5. **Max Rounds** - Safety valve (default: 5 rounds)
|
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.
|
The 💙 can also **extend** the dialogue if it sees unincorporated perspectives that no 🧁 has surfaced.
|
||||||
|
|
||||||
|
|
|
||||||
888
.blue/docs/rfcs/0057-judge-convergence-discipline.approved.md
Normal file
888
.blue/docs/rfcs/0057-judge-convergence-discipline.approved.md
Normal file
|
|
@ -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/<name>/`
|
||||||
|
|
||||||
|
**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/<name>/`
|
||||||
|
|
||||||
|
```
|
||||||
|
.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:** `<ISO-8601>-<topic>/`
|
||||||
|
|
||||||
|
Format: `YYYY-MM-DDTHHMMZ-<topic>` (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/<ISO>-<name>`
|
||||||
|
- [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/<ISO>-<name>/` 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."*
|
||||||
|
|
@ -1048,21 +1048,93 @@ pub fn get_experts(
|
||||||
Ok(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(
|
pub fn create_round(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
dialogue_id: &str,
|
dialogue_id: &str,
|
||||||
round: i32,
|
round: i32,
|
||||||
title: Option<&str>,
|
title: Option<&str>,
|
||||||
score: i32,
|
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> {
|
) -> Result<(), AlignmentDbError> {
|
||||||
let now = Utc::now().to_rfc3339();
|
let now = Utc::now().to_rfc3339();
|
||||||
|
|
||||||
|
let sc = score_components.cloned().unwrap_or_default();
|
||||||
|
let cm = convergence.cloned().unwrap_or_default();
|
||||||
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO alignment_rounds
|
"INSERT INTO alignment_rounds
|
||||||
(dialogue_id, round, title, score, status, created_at)
|
(dialogue_id, round, title, score, score_wisdom, score_consistency, score_truth, score_relationships,
|
||||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
|
open_tensions, new_perspectives, converge_signals, panel_size, status, created_at)
|
||||||
params![dialogue_id, round, title, score, "open", now],
|
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
|
// Update dialogue total rounds
|
||||||
|
|
@ -1075,6 +1147,134 @@ pub fn create_round(
|
||||||
Ok(())
|
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<Vec<String>, 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::<Result<Vec<String>, _>>()?;
|
||||||
|
|
||||||
|
Ok(signals)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RFC 0057: Get scoreboard data for a dialogue
|
||||||
|
pub fn get_scoreboard(
|
||||||
|
conn: &Connection,
|
||||||
|
dialogue_id: &str,
|
||||||
|
) -> Result<Vec<ScoreboardRow>, 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::<Result<Vec<_>, _>>()?;
|
||||||
|
|
||||||
|
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<String>), 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
|
/// Get next sequence number for an entity type in a round
|
||||||
pub fn next_seq(
|
pub fn next_seq(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
|
|
@ -2226,6 +2426,16 @@ mod tests {
|
||||||
round INTEGER NOT NULL,
|
round INTEGER NOT NULL,
|
||||||
title TEXT,
|
title TEXT,
|
||||||
score INTEGER NOT NULL,
|
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,
|
summary TEXT,
|
||||||
status TEXT NOT NULL DEFAULT 'open',
|
status TEXT NOT NULL DEFAULT 'open',
|
||||||
created_at TEXT NOT NULL,
|
created_at TEXT NOT NULL,
|
||||||
|
|
@ -2233,6 +2443,15 @@ mod tests {
|
||||||
PRIMARY KEY (dialogue_id, round)
|
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 (
|
CREATE TABLE alignment_perspectives (
|
||||||
dialogue_id TEXT NOT NULL,
|
dialogue_id TEXT NOT NULL,
|
||||||
round INTEGER 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_dialogue ON alignment_refs(dialogue_id);
|
||||||
CREATE INDEX idx_refs_target ON alignment_refs(dialogue_id, target_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_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();
|
.unwrap();
|
||||||
|
|
@ -3661,4 +3905,226 @@ mod tests {
|
||||||
assert_eq!(ref_row.0, p1);
|
assert_eq!(ref_row.0, p1);
|
||||||
assert_eq!(ref_row.1, t1);
|
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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,14 @@ use blue_core::alignment_db::{
|
||||||
ValidationError, ValidationCollector,
|
ValidationError, ValidationCollector,
|
||||||
validate_ref_semantics, validate_display_id,
|
validate_ref_semantics, validate_display_id,
|
||||||
get_dialogue, register_expert, get_experts,
|
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_recommendation, register_evidence, register_claim, register_ref,
|
||||||
register_verdict, update_tension_status, update_expert_score,
|
register_verdict, update_tension_status, update_expert_score,
|
||||||
get_perspectives, get_tensions, get_recommendations, get_evidence, get_claims, get_verdicts,
|
get_perspectives, get_tensions, get_recommendations, get_evidence, get_claims, get_verdicts,
|
||||||
display_id, parse_display_id,
|
display_id, parse_display_id,
|
||||||
|
// RFC 0057: Convergence discipline
|
||||||
|
ScoreComponents, ConvergenceMetrics,
|
||||||
|
can_dialogue_converge, get_scoreboard,
|
||||||
};
|
};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -557,8 +560,9 @@ pub fn handle_create(state: &mut ProjectState, args: &Value) -> Result<Value, Se
|
||||||
|
|
||||||
// Build response — RFC 0023: inject protocol as prose in message field
|
// Build response — RFC 0023: inject protocol as prose in message field
|
||||||
let (message, judge_protocol) = if let Some(ref agents) = pastry_agents {
|
let (message, judge_protocol) = if let Some(ref agents) = pastry_agents {
|
||||||
// RFC 0029: Create output directory for file-based subagent output
|
// RFC 0057: Create output directory in .blue/dialogues/<ISO>-<name>/
|
||||||
let output_dir = format!("/tmp/blue-dialogue/{}", slug);
|
// 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| {
|
fs::create_dir_all(&output_dir).map_err(|e| {
|
||||||
ServerError::CommandFailed(format!("Failed to create output dir {}: {}", output_dir, 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 Perspectives Inventory (one row per [PERSPECTIVE Pnn:] marker)
|
||||||
- Updated Tensions Tracker (one row per [TENSION Tn:] 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).
|
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
|
7. CONVERGENCE CHECK (RFC 0057) — BOTH CONDITIONS REQUIRED:
|
||||||
Otherwise, start next round (agents will read Step 5 artifacts via CONTEXT_INSTRUCTIONS).
|
a. Calculate VELOCITY = open_tensions + new_perspectives
|
||||||
Maximum 5 rounds (safety valve)
|
- 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 ===
|
=== TOKEN BUDGET ===
|
||||||
|
|
||||||
|
|
@ -1352,6 +1384,7 @@ Both well under 25K limit. Opus usage minimized.
|
||||||
|
|
||||||
AGENTS: {agent_names}
|
AGENTS: {agent_names}
|
||||||
OUTPUT DIR: {output_dir}
|
OUTPUT DIR: {output_dir}
|
||||||
|
MAX ROUNDS: 10
|
||||||
|
|
||||||
FORMAT RULES — MANDATORY:
|
FORMAT RULES — MANDATORY:
|
||||||
- ALWAYS prefix agent names with their emoji (🧁 Muffin) not bare name (Muffin)
|
- 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
|
- Expert Panel table columns: Agent | Role | Tier | Relevance | Emoji
|
||||||
- Round headers use emoji prefix (### 🧁 Muffin)
|
- Round headers use emoji prefix (### 🧁 Muffin)
|
||||||
- Scores start at 0 — only fill after reading agent returns
|
- 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 0: No context instructions (agents have no memory of each other)
|
||||||
- Round 1+: Automatically includes READ CONTEXT block with correct paths"##,
|
- Round 1+: Automatically includes READ CONTEXT block with correct paths"##,
|
||||||
agent_count = agents.len(),
|
agent_count = agents.len(),
|
||||||
|
|
@ -1438,10 +1472,14 @@ You are not limited to the initial pool. If the dialogue surfaces a perspective
|
||||||
"sources": sources,
|
"sources": sources,
|
||||||
"output_dir": output_dir,
|
"output_dir": output_dir,
|
||||||
"rotation": format!("{:?}", rotation).to_lowercase(),
|
"rotation": format!("{:?}", rotation).to_lowercase(),
|
||||||
|
// RFC 0057: Updated convergence params
|
||||||
"convergence": {
|
"convergence": {
|
||||||
"max_rounds": 5,
|
"max_rounds": 10,
|
||||||
"velocity_threshold": 0.1,
|
|
||||||
"tension_resolution_gate": true,
|
"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<Value,
|
||||||
let verdicts = get_verdicts(conn, dialogue_id)
|
let verdicts = get_verdicts(conn, dialogue_id)
|
||||||
.map_err(|e| ServerError::CommandFailed(format!("Failed to get verdicts: {}", e)))?;
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to get verdicts: {}", e)))?;
|
||||||
|
|
||||||
|
// RFC 0057: Get scoreboard and convergence status
|
||||||
|
let scoreboard = get_scoreboard(conn, dialogue_id)
|
||||||
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to get scoreboard: {}", e)))?;
|
||||||
|
|
||||||
|
let (can_converge, blockers) = can_dialogue_converge(conn, dialogue_id)
|
||||||
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to check convergence: {}", e)))?;
|
||||||
|
|
||||||
|
let last_round_metrics = scoreboard.last().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,
|
||||||
|
}));
|
||||||
|
|
||||||
// Build context response
|
// Build context response
|
||||||
Ok(json!({
|
Ok(json!({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
|
|
@ -2059,6 +2122,12 @@ pub fn handle_round_context(state: &ProjectState, args: &Value) -> Result<Value,
|
||||||
"total_rounds": dialogue.total_rounds,
|
"total_rounds": dialogue.total_rounds,
|
||||||
"total_alignment": dialogue.total_alignment,
|
"total_alignment": dialogue.total_alignment,
|
||||||
},
|
},
|
||||||
|
// RFC 0057: Convergence status
|
||||||
|
"convergence": {
|
||||||
|
"can_converge": can_converge,
|
||||||
|
"blockers": blockers,
|
||||||
|
"last_round": last_round_metrics,
|
||||||
|
},
|
||||||
"experts": experts.iter().map(|e| json!({
|
"experts": experts.iter().map(|e| json!({
|
||||||
"expert_slug": e.expert_slug,
|
"expert_slug": e.expert_slug,
|
||||||
"role": e.role,
|
"role": e.role,
|
||||||
|
|
@ -2407,6 +2476,33 @@ pub fn handle_round_register(state: &ProjectState, args: &Value) -> Result<Value
|
||||||
|
|
||||||
let summary = args.get("summary").and_then(|v| v.as_str());
|
let summary = args.get("summary").and_then(|v| v.as_str());
|
||||||
|
|
||||||
|
// RFC 0057: Parse score components (W/C/T/R breakdown)
|
||||||
|
let score_components = args.get("score_components").map(|sc| ScoreComponents {
|
||||||
|
wisdom: sc.get("wisdom").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
|
||||||
|
consistency: sc.get("consistency").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
|
||||||
|
truth: sc.get("truth").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
|
||||||
|
relationships: sc.get("relationships").and_then(|v| v.as_i64()).unwrap_or(0) as i32,
|
||||||
|
});
|
||||||
|
|
||||||
|
// RFC 0057: Parse convergence metrics
|
||||||
|
let convergence_metrics = {
|
||||||
|
let open_tensions = args.get("open_tensions").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||||
|
let new_perspectives = args.get("new_perspectives").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||||
|
let converge_signals = args.get("converge_signals").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||||
|
let panel_size = args.get("panel_size").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||||
|
|
||||||
|
if open_tensions > 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
|
// Phase 2c: Batch validation - collect ALL errors before registration
|
||||||
let validation_errors = validate_round_register_inputs(args);
|
let validation_errors = validate_round_register_inputs(args);
|
||||||
if !validation_errors.is_empty() {
|
if !validation_errors.is_empty() {
|
||||||
|
|
@ -2419,9 +2515,17 @@ pub fn handle_round_register(state: &ProjectState, args: &Value) -> Result<Value
|
||||||
let _dialogue = get_dialogue(conn, dialogue_id)
|
let _dialogue = get_dialogue(conn, dialogue_id)
|
||||||
.map_err(|e| ServerError::CommandFailed(format!("Dialogue not found: {}", e)))?;
|
.map_err(|e| ServerError::CommandFailed(format!("Dialogue not found: {}", e)))?;
|
||||||
|
|
||||||
// Create round record (title, then score)
|
// Create round record with RFC 0057 metrics
|
||||||
create_round(conn, dialogue_id, round, summary, score)
|
create_round_with_metrics(
|
||||||
.map_err(|e| ServerError::CommandFailed(format!("Failed to create round: {}", e)))?;
|
conn,
|
||||||
|
dialogue_id,
|
||||||
|
round,
|
||||||
|
summary,
|
||||||
|
score,
|
||||||
|
score_components.as_ref(),
|
||||||
|
convergence_metrics.as_ref(),
|
||||||
|
)
|
||||||
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to create round: {}", e)))?;
|
||||||
|
|
||||||
let mut registered = json!({
|
let mut registered = json!({
|
||||||
"perspectives": [],
|
"perspectives": [],
|
||||||
|
|
@ -2700,12 +2804,53 @@ pub fn handle_verdict_register(state: &ProjectState, args: &Value) -> Result<Val
|
||||||
.and_then(|v| v.as_array())
|
.and_then(|v| v.as_array())
|
||||||
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());
|
.map(|arr| arr.iter().filter_map(|v| v.as_str().map(String::from)).collect());
|
||||||
|
|
||||||
|
// RFC 0057: Force flag allows convergence without meeting criteria (with warning)
|
||||||
|
let force = coerce_bool(args.get("force").unwrap_or(&Value::Bool(false))).unwrap_or(false);
|
||||||
|
|
||||||
let conn = state.store.conn();
|
let conn = state.store.conn();
|
||||||
|
|
||||||
// Verify dialogue exists
|
// Verify dialogue exists
|
||||||
let _dialogue = get_dialogue(conn, dialogue_id)
|
let _dialogue = get_dialogue(conn, dialogue_id)
|
||||||
.map_err(|e| ServerError::CommandFailed(format!("Dialogue not found: {}", e)))?;
|
.map_err(|e| ServerError::CommandFailed(format!("Dialogue not found: {}", e)))?;
|
||||||
|
|
||||||
|
// RFC 0057: Convergence validation for final verdicts
|
||||||
|
let mut forced_warning: Option<String> = 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 {
|
let verdict = Verdict {
|
||||||
dialogue_id: dialogue_id.to_string(),
|
dialogue_id: dialogue_id.to_string(),
|
||||||
verdict_id: verdict_id.to_string(),
|
verdict_id: verdict_id.to_string(),
|
||||||
|
|
@ -2730,14 +2875,22 @@ pub fn handle_verdict_register(state: &ProjectState, args: &Value) -> Result<Val
|
||||||
register_verdict(conn, &verdict)
|
register_verdict(conn, &verdict)
|
||||||
.map_err(|e| ServerError::CommandFailed(format!("Failed to register verdict: {}", e)))?;
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to register verdict: {}", e)))?;
|
||||||
|
|
||||||
Ok(json!({
|
// RFC 0057: Include warning if convergence was forced
|
||||||
|
let mut response = json!({
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"message": format!("Registered {} verdict '{}' for dialogue '{}'", verdict_type.as_str(), verdict_id, dialogue_id),
|
"message": format!("Registered {} verdict '{}' for dialogue '{}'", verdict_type.as_str(), verdict_id, dialogue_id),
|
||||||
"dialogue_id": dialogue_id,
|
"dialogue_id": dialogue_id,
|
||||||
"verdict_id": verdict_id,
|
"verdict_id": verdict_id,
|
||||||
"verdict_type": verdict_type.as_str(),
|
"verdict_type": verdict_type.as_str(),
|
||||||
"round": round,
|
"round": round,
|
||||||
}))
|
});
|
||||||
|
|
||||||
|
if let Some(warning) = forced_warning {
|
||||||
|
response["warning"] = json!(warning);
|
||||||
|
response["forced"] = json!(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle blue_dialogue_export (RFC 0051)
|
/// Handle blue_dialogue_export (RFC 0051)
|
||||||
|
|
@ -2779,6 +2932,10 @@ pub fn handle_export(state: &ProjectState, args: &Value) -> Result<Value, Server
|
||||||
let verdicts = get_verdicts(conn, dialogue_id)
|
let verdicts = get_verdicts(conn, dialogue_id)
|
||||||
.map_err(|e| ServerError::CommandFailed(format!("Failed to get verdicts: {}", e)))?;
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to get verdicts: {}", e)))?;
|
||||||
|
|
||||||
|
// RFC 0057: Get scoreboard with full metrics
|
||||||
|
let scoreboard = get_scoreboard(conn, dialogue_id)
|
||||||
|
.map_err(|e| ServerError::CommandFailed(format!("Failed to get scoreboard: {}", e)))?;
|
||||||
|
|
||||||
// Build export structure
|
// Build export structure
|
||||||
let export_data = json!({
|
let export_data = json!({
|
||||||
"dialogue": {
|
"dialogue": {
|
||||||
|
|
@ -2883,6 +3040,30 @@ pub fn handle_export(state: &ProjectState, args: &Value) -> Result<Value, Server
|
||||||
"supporting_experts": v.supporting_experts,
|
"supporting_experts": v.supporting_experts,
|
||||||
"created_at": v.created_at.to_rfc3339(),
|
"created_at": v.created_at.to_rfc3339(),
|
||||||
})).collect::<Vec<_>>(),
|
})).collect::<Vec<_>>(),
|
||||||
|
// 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::<Vec<_>>(),
|
||||||
"exported_at": chrono::Utc::now().to_rfc3339(),
|
"exported_at": chrono::Utc::now().to_rfc3339(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3170,9 +3351,11 @@ mod tests {
|
||||||
// Must have output_dir
|
// Must have output_dir
|
||||||
assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/system-design");
|
assert_eq!(protocol["output_dir"], "/tmp/blue-dialogue/system-design");
|
||||||
|
|
||||||
// Must have convergence params
|
// Must have convergence params (RFC 0057)
|
||||||
assert_eq!(protocol["convergence"]["max_rounds"], 5);
|
assert_eq!(protocol["convergence"]["max_rounds"], 10);
|
||||||
assert!(protocol["convergence"]["tension_resolution_gate"].as_bool().unwrap());
|
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -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 |
|
| `--panel-size` | pool size or 12 | Number of experts per round |
|
||||||
| `--rotation` | `graduated` | Rotation mode: **graduated** (default), none, wildcards, full |
|
| `--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 |
|
| `--rfc` | none | Link dialogue to an RFC |
|
||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
@ -75,7 +75,7 @@ The suggested panel is just that — a suggestion. **Review it before Round 0:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"output_dir": "/tmp/blue-dialogue/data-design",
|
"output_dir": ".blue/dialogues/data-design",
|
||||||
"round": 0,
|
"round": 0,
|
||||||
"panel": [
|
"panel": [
|
||||||
{ "name": "Muffin", "role": "API Architect", "source": "pool" },
|
{ "name": "Muffin", "role": "API Architect", "source": "pool" },
|
||||||
|
|
@ -105,7 +105,7 @@ Use `blue_dialogue_evolve_panel` to specify your panel:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"output_dir": "/tmp/blue-dialogue/investment-strategy",
|
"output_dir": ".blue/dialogues/investment-strategy",
|
||||||
"round": 1,
|
"round": 1,
|
||||||
"panel": [
|
"panel": [
|
||||||
{ "name": "Muffin", "role": "Value Analyst", "source": "retained" },
|
{ "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(
|
blue_dialogue_round_prompt(
|
||||||
output_dir="/tmp/blue-dialogue/investment-strategy",
|
output_dir=".blue/dialogues/investment-strategy",
|
||||||
agent_name="Palmier",
|
agent_name="Palmier",
|
||||||
agent_emoji="🧁",
|
agent_emoji="🧁",
|
||||||
agent_role="Geopolitical Risk Analyst",
|
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
|
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
|
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
|
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
|
## The Spirit of the Dialogue
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue