Skip to content

Commit 81fc05f

Browse files
committed
release: v1.1.4 - Phase 4 insight preservation and local-first config
Changed: - Local-first config loading (./.env takes priority over ~/.quorum/.env) Fixed: - Phase 4 now receives full Phase 3 discussion context - Prompt encourages preserving thresholds/frameworks from discussion - Socratic: "be concise" → "be thorough" - Selected models filtered against available models
1 parent 0636b01 commit 81fc05f

10 files changed

Lines changed: 115 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
---
1111

12+
## [1.1.4] - 2025-12-22
13+
14+
### Changed
15+
16+
- **Local-first config loading** - When `./.env` exists in current directory, use it exclusively
17+
- Previously loaded both `~/.quorum/.env` and `./.env` and merged them
18+
- Now uses local-first priority: local `.env` if present, otherwise global `~/.quorum/.env`
19+
- Prevents confusing merged model lists when running from different project directories
20+
21+
### Fixed
22+
23+
- **Phase 4 Insight Preservation** - Final positions now include full discussion context
24+
- Previously, Phase 3 discussion was not passed to Phase 4, causing models to lose valuable insights
25+
- Now `_discussion_messages` stores the complete Phase 3 discussion and passes it to `get_final_position_prompt()`
26+
- Prompt updated to encourage including "specific thresholds, frameworks, or conditions" from discussion
27+
- Removed "be concise" instruction that triggered information loss
28+
29+
- **Socratic Method Prompt** - Changed "Be concise but complete" to "Be thorough and complete"
30+
- Prevents respondents from over-compressing their answers
31+
32+
- **Selected models filtering** - Cached selected models are now filtered against available models
33+
- Previously, switching configs could show stale models from old config
34+
- Now `selected_models` in `~/.quorum/settings.json` is filtered to only show available models
35+
36+
---
37+
1238
## [1.1.3] - 2025-12-21
1339

1440
### Changed

frontend/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/components/Header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Box, Text } from "ink";
77
import { useStore } from "../store/index.js";
88
import { t } from "../i18n/index.js";
99

10-
const VERSION = "1.1.3";
10+
const VERSION = "1.1.4";
1111

1212
export function Header() {
1313
const { selectedModels, discussionMethod, availableModels } = useStore();

src/quorum/agents.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,7 @@ def get_advocate_verdict_prompt(discussion_history: str) -> str:
550550
2. Explain your reasoning step by step
551551
3. Acknowledge uncertainty where it exists
552552
4. If the question reveals a flaw in your thinking, admit it honestly
553-
5. Be concise but complete
553+
5. Be thorough and complete
554554
555555
CONTEXT FROM INITIAL ANSWERS:
556556
{all_initial_answers}
@@ -717,15 +717,19 @@ def get_standard_discussion_prompt(
717717
ORIGINAL QUESTION:
718718
{original_question}
719719
720-
State YOUR final position after considering all perspectives.
720+
DISCUSSION SUMMARY:
721+
{discussion_summary}
722+
723+
State YOUR final position, incorporating what you learned from the discussion.
721724
722725
IMPORTANT:
726+
- Include specific thresholds, frameworks, or conditions that emerged from the discussion
727+
- Reference key points of agreement or disagreement
723728
- State your position only - do NOT ask for user input
724729
- Do NOT end with "let me know if you want more details"
725-
- This is your FINAL conclusion
726730
727731
Format EXACTLY as:
728-
POSITION: [your final answer - be concise]
732+
POSITION: [your refined final answer, incorporating discussion insights]
729733
CONFIDENCE: [HIGH/MEDIUM/LOW]
730734
731735
Where:
@@ -821,18 +825,20 @@ def get_critique_prompt(
821825
)
822826

823827

824-
def get_final_position_prompt(original_question: str) -> str:
828+
def get_final_position_prompt(original_question: str, discussion_summary: str = "") -> str:
825829
"""Get the final position prompt (Phase 4).
826830
827831
Args:
828832
original_question: The original question being discussed.
833+
discussion_summary: Summary of Phase 3 discussion to provide context.
829834
830835
Returns:
831836
Formatted final position prompt.
832837
"""
833838
return FINAL_POSITION_PROMPT.format(
834839
language_instruction=get_language_instruction(),
835840
original_question=original_question,
841+
discussion_summary=discussion_summary if discussion_summary else "[No discussion summary available]",
836842
)
837843

838844

src/quorum/config.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from pydantic import Field, field_validator
1313
from pydantic_settings import BaseSettings, SettingsConfigDict
14+
from pydantic_settings.sources import DotEnvSettingsSource
1415

1516
# API key validation patterns and lengths
1617
API_KEY_MIN_LENGTH = 20 # Minimum reasonable API key length
@@ -169,15 +170,49 @@ def add_to_input_history(input_text: str, max_items: int = 100) -> None:
169170
_write_secure_file(INPUT_HISTORY_CACHE, json.dumps({"history": history}))
170171

171172

173+
def _get_active_env_file() -> Path:
174+
"""Get the active .env file using local-first priority.
175+
176+
If ./.env exists in current directory, use it exclusively.
177+
Otherwise fall back to ~/.quorum/.env for global config.
178+
"""
179+
local_env = Path(".env")
180+
if local_env.exists():
181+
return local_env
182+
return CACHE_DIR / ".env"
183+
184+
172185
class Settings(BaseSettings):
173186
"""Application settings loaded from environment variables."""
174187

175188
model_config = SettingsConfigDict(
176-
env_file=(CACHE_DIR / ".env", ".env"), # ~/.quorum/.env first, then ./.env
177189
env_file_encoding="utf-8",
178190
extra="ignore",
179191
)
180192

193+
@classmethod
194+
def settings_customise_sources(
195+
cls,
196+
settings_cls: type[BaseSettings],
197+
init_settings,
198+
env_settings,
199+
dotenv_settings,
200+
file_secret_settings,
201+
):
202+
"""Use local .env if exists, otherwise global ~/.quorum/.env.
203+
204+
Local-first priority: If ./.env exists in the current working directory,
205+
use it exclusively. Otherwise fall back to ~/.quorum/.env for global config.
206+
This prevents confusing merged configs when running from different directories.
207+
"""
208+
env_file = _get_active_env_file()
209+
dotenv_settings = DotEnvSettingsSource(
210+
settings_cls,
211+
env_file=env_file,
212+
env_file_encoding="utf-8",
213+
)
214+
return (init_settings, env_settings, dotenv_settings, file_secret_settings)
215+
181216
# API Keys
182217
openai_api_key: str | None = Field(default=None, alias="OPENAI_API_KEY")
183218
anthropic_api_key: str | None = Field(default=None, alias="ANTHROPIC_API_KEY")

src/quorum/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
# Version Information
88
# =============================================================================
99

10-
__version__ = "1.1.3"
10+
__version__ = "1.1.4"
1111
"""Quorum application version."""
1212

1313
PROTOCOL_VERSION = "1.0.0"

src/quorum/ipc.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -667,9 +667,24 @@ async def _handle_get_user_settings(self, params: dict) -> dict:
667667
"""Handle get_user_settings request.
668668
669669
Returns cached user settings (selected_models, method, synthesizer, etc).
670+
Filters selected_models to only include models available in current config.
670671
"""
671672
from .config import get_user_settings
672-
return get_user_settings()
673+
from .providers import list_all_models_sync
674+
675+
settings = get_user_settings()
676+
677+
# Filter selected_models to only include currently available models
678+
if "selected_models" in settings:
679+
all_models = list_all_models_sync()
680+
available_ids = {
681+
m.id for models in all_models.values() for m in models
682+
}
683+
settings["selected_models"] = [
684+
m for m in settings["selected_models"] if m in available_ids
685+
]
686+
687+
return settings
673688

674689
async def _handle_save_user_settings(self, params: dict) -> dict:
675690
"""Handle save_user_settings request.

src/quorum/mcp/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,7 +373,7 @@ async def _run_server() -> None:
373373
write,
374374
InitializationOptions(
375375
server_name="quorum",
376-
server_version="1.1.3",
376+
server_version="1.1.4",
377377
capabilities=server.get_capabilities(
378378
notification_options=NotificationOptions(),
379379
experimental_capabilities={},

src/quorum/methods/standard.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(self, *args, **kwargs):
6969
self._initial_responses: dict[str, str] = {}
7070
self._critiques: dict[str, CritiqueResponse] = {}
7171
self._final_positions: list[FinalPosition] = []
72+
self._discussion_messages: list[str] = [] # Store Phase 3 discussion for Phase 4
7273

7374
async def run_stream(self, task: str) -> AsyncIterator[MessageType]:
7475
"""Run 5-phase consensus discussion."""
@@ -344,7 +345,9 @@ async def _run_phase3_discussion(self, task: str) -> AsyncIterator[ThinkingIndic
344345
user_msg = f"Question: {task}\n\nContribute to the discussion."
345346
response = await self._get_model_response(model_id, prompt, user_msg)
346347

347-
discussion_history.append(f"{self._display_name(model_id)}:\n{response}")
348+
message_text = f"{self._display_name(model_id)}:\n{response}"
349+
discussion_history.append(message_text)
350+
self._discussion_messages.append(message_text) # Store for Phase 4
348351

349352
yield self._create_team_message(model_id, response)
350353

@@ -354,7 +357,9 @@ async def _run_phase3_discussion(self, task: str) -> AsyncIterator[ThinkingIndic
354357

355358
async def _run_phase4_final_positions(self, task: str) -> list[FinalPosition]:
356359
"""Phase 4: Get final positions with confidence from all models."""
357-
final_prompt = get_final_position_prompt(task)
360+
# Build discussion summary from Phase 3
361+
discussion_summary = "\n\n".join(self._discussion_messages) if self._discussion_messages else ""
362+
final_prompt = get_final_position_prompt(task, discussion_summary)
358363

359364
async def get_final_position(model_id: str) -> FinalPosition:
360365
try:

tests/test_config.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,8 +181,12 @@ class TestSettings:
181181
"""
182182

183183
@pytest.fixture(autouse=True)
184-
def clear_env(self, monkeypatch):
185-
"""Clear all API key environment variables for isolated tests."""
184+
def clear_env(self, monkeypatch, tmp_path):
185+
"""Clear all API key environment variables for isolated tests.
186+
187+
Also mocks _get_active_env_file to return a non-existent path,
188+
preventing tests from picking up real .env files.
189+
"""
186190
env_vars = [
187191
"OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "XAI_API_KEY",
188192
"OPENAI_MODELS", "ANTHROPIC_MODELS", "GOOGLE_MODELS", "XAI_MODELS",
@@ -192,6 +196,14 @@ def clear_env(self, monkeypatch):
192196
for var in env_vars:
193197
monkeypatch.delenv(var, raising=False)
194198

199+
# Mock _get_active_env_file to return non-existent path
200+
# This prevents tests from loading real .env files
201+
monkeypatch.setattr(
202+
quorum.config,
203+
"_get_active_env_file",
204+
lambda: tmp_path / ".env.nonexistent",
205+
)
206+
195207
def test_get_models_empty(self):
196208
"""Test getting models when none configured."""
197209
settings = Settings(_env_file=None)

0 commit comments

Comments
 (0)